<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Welcome! Everything is fine.</title>
    <link>https://3uomlkh.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 11 Sep 2026 00:55:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>개발곰발</managingEditor>
    <image>
      <title>Welcome! Everything is fine.</title>
      <url>https://tistory1.daumcdn.net/tistory/4817209/attach/c8c59cd6f9e34ff2aae8b90ea3302381</url>
      <link>https://3uomlkh.tistory.com</link>
    </image>
    <item>
      <title>Claude Cowork Skill로 주간/월간 취업 리포트 자동화하기</title>
      <link>https://3uomlkh.tistory.com/413</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;689&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkoZ34/dJMcafALiBE/irrxTPFRqsfK5QdnsyKy7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkoZ34/dJMcafALiBE/irrxTPFRqsfK5QdnsyKy7k/img.png&quot; data-alt=&quot;클로드에서 스킬을 사용하는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkoZ34/dJMcafALiBE/irrxTPFRqsfK5QdnsyKy7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkoZ34%2FdJMcafALiBE%2FirrxTPFRqsfK5QdnsyKy7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;756&quot; height=&quot;689&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;689&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클로드에서 스킬을 사용하는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;반복되는 취업 지원 관리가 번거로웠다 &lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;302&quot; data-start=&quot;204&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;취업 준비를 하면서 지원 현황은 모두 Notion DB에 기록하고 있었다. 지원한 회사, 마감일, 지원일, 진행 상태 등을 한곳에서 관리할 수 있어 기록 자체는 어렵지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;302&quot; data-start=&quot;204&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;334&quot; data-start=&quot;307&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;문제는 &lt;b&gt;기록하는 것보다 관리하는 것&lt;/b&gt;이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;334&quot; data-start=&quot;307&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;499&quot; data-start=&quot;339&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;지원하는 회사가 하나둘 늘어나자 매번 Notion을 열어 어떤 공고의 마감이 얼마 남았는지, 어떤 회사가 30일 넘게 연락이 없는지 직접 확인해야 했다. 합격&amp;middot;불합격 메일을 받으면 상태도 수동으로 수정해야 했는데, 가끔 이를 놓쳐 실제 진행 상황과 Notion의 내용이 달라지기도 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;499&quot; data-start=&quot;339&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;518&quot; data-start=&quot;504&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;문득 이런 생각이 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-end=&quot;589&quot; data-start=&quot;523&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;589&quot; data-start=&quot;525&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #666666;&quot;&gt;&lt;i&gt;&quot;Claude는 Notion도 읽을 수 있고 Gmail도 읽을 수 있는데, 이걸 내가 직접 관리할 이유가 있을까?&quot;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;646&quot; data-start=&quot;594&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;646&quot; data-start=&quot;594&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그렇게 반복적인 취업 지원 관리 업무를 &lt;b&gt;Claude Cowork Skill&lt;/b&gt;로 자동화하기 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Claude Cowork Skill로 자동화 구조 만들기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;284&quot; data-start=&quot;262&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;자동화 흐름은 크게 세 단계로 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;/weekly-report
     │
     ▼
Notion DB 조회
     │
     ▼
지원 현황 집계 및 분석
     │
     ▼
Gmail 리포트 작성&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;471&quot; data-start=&quot;391&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Notion DB에서 지원 현황 데이터를 가져온 뒤, 현재 진행 상황과 확인이 필요한 항목을 정리하고 Gmail 리포트 형태로 발송하는 구조다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;흥미로웠던 점은 Skill 파일 자체도 직접 작성하지 않았다는 것이다. Claude에게 자연어로 원하는 동작을 설명하면, 이를 실행 가능한 &lt;b&gt;SKILL.md&lt;/b&gt; 형태로 구조화해준다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-end=&quot;651&quot; data-start=&quot;585&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;651&quot; data-start=&quot;587&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #666666;&quot;&gt;&lt;i&gt;&quot;Notion에서 지난 7일 데이터 가져와서 집계하고, 마감 임박 항목은 리마인드로 강조해서 Gmail로 보내줘.&quot;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;786&quot; data-start=&quot;653&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;786&quot; data-start=&quot;653&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 이렇게 요구사항을 전달하면 Claude가 아래와 같은 Skill 명세를 만들어준다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1785485614350&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;---
name: weekly-report
description: &amp;gt;
  취업 지원 현황 주간 리포트를 생성하고 Gmail로 발송하는 스킬.
  사용자가 &quot;주간 리포트&quot;, &quot;weekly report&quot; 등을 요청하면 실행한다.
---

## 목적
Notion DB에서 지난 7일간의 취업 지원 데이터를 집계하고,
30일 이상 무응답 항목을 정리한 뒤 Gmail로 리포트를 발송한다.


## Notion DB 정보
(...이하 생략)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;887&quot; data-start=&quot;832&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;생성된 Skill을 설치하면 이후에는 &lt;b&gt;/weekly-report&lt;/b&gt; 명령 한 번으로 Notion 조회부터 리포트 생성, Gmail 발송까지 전체 흐름이 실행된다. Skill을 만들면서 가장 많이 한 일은 코드를 작성하는 것이 아니라 요구사항을 다듬는 것이었다. 처음에는 &quot;주간 리포트를 보내줘&quot; 수준이었지만, 사용하면서 필요한 규칙을 하나씩 추가했다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;마감 7일 이내 항목 리마인드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;서류 제출 후 30일 이상 무응답 항목 자동 처리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;신규 지원 / 진행 중 / 탈락 상태별 구분&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;표시할 항목이 없는 섹션은 생략&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;새로운 요구사항을 설명하면 Claude가 이를 Skill 문서에 반영해 주었고, Skill도 점점 실제 사용에 맞게 발전했다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;처음에는 매주 일요일 아침에 자동으로 리포트를 보내는 스케줄 실행을 고려했다. 하지만 컴퓨터가 꺼져 있으면 실행되지 않는다는 제약이 있어, 필요할 때 /weekly-report, /monthly-report 명령으로 실행하는 온디맨드 방식으로 변경했다. 발송 과정에서는 Gmail MCP의 발송 기능이 제한적이었기 때문에, Claude in Chrome을 활용해 Gmail 작성창을 직접 열고 리포트를 입력하는 방식으로 구현했다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; 구현보다 디버깅이 더 오래 걸렸다 &lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;356&quot; data-start=&quot;311&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;구현 과정에서 가장 오래 고민했던 부분은 &lt;b&gt;외부 서비스와의 연동 방식&lt;/b&gt;이었다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;377&quot; data-start=&quot;358&quot; data-section-id=&quot;tkp1zg&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1. Notion 조회 방식&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;464&quot; data-start=&quot;379&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;처음에는 SQL 쿼리로 Notion DB를 직접 조회하려고 했다. 하지만 해당 기능이 Business 플랜 이상에서만 지원된다는 것을 뒤늦게 알게 됐다. 결국 notion-search로 항목 목록을 조회한 뒤 notion-fetch로 개별 데이터를 가져오는 방식으로 우회했다. 현재는 정상적으로 동작하지만, 데이터가 많아질수록 여러 항목을 개별 조회해야 한다는 한계가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;621&quot; data-start=&quot;596&quot; data-section-id=&quot;urq1u0&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2. Gmail 본문이 비어 있는 문제&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;가장 오래 헤맨 부분은 Gmail 자동 작성이었다. 처음에는 Gmail의 접근성용 textarea에 리포트 내용을 입력하면 실제 메일 본문에도 반영될 것이라고 생각했다. 하지만 발송된 메일은 계속 빈 본문으로 도착했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;확인해보니 Gmail은 화면에 표시되는 리치 텍스트 에디터(contenteditable)와 접근성용 textarea를 분리해서 관리하고 있었다. textarea의 값을 변경하는 것은 화면에 보이는 값만 바꾸는 것이었고, Gmail 내부 상태에는 반영되지 않아 실제 발송 시 본문이 비어 있었던 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해결 방법은 값을 직접 주입하는 것이 아니라, 사용자가 직접 붙여넣기한 것처럼 입력 이벤트를 발생시키는 것이었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const richEditor = document.querySelector('[contenteditable=&quot;true&quot;][aria-multiline=&quot;true&quot;]');

if (richEditor) {
  richEditor.focus();
  const dt = new DataTransfer();
  dt.setData('text/plain', reportText);

  richEditor.dispatchEvent(new ClipboardEvent('paste', {
    bubbles: true,
    cancelable: true,
    clipboardData: dt
  }));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1516&quot; data-start=&quot;1434&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;1516&quot; data-start=&quot;1434&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DataTransfer를 이용해 실제 paste 이벤트를 발생시키자 Gmail이 이를 사용자 입력으로 인식했고, 정상적으로 본문이 작성됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;1660&quot; data-start=&quot;1518&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이번 경험을 통해 &lt;b&gt;DOM 요소의 값을 변경하는 것과 서비스가 실제 입력으로 인식하는 것은 다를 수 있다&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 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 현재는 실제로 /weekly-report와 /monthly-report를 실행해 취업 지원 현황 리포트를 받아보고 있다. &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;1842&quot; data-origin-height=&quot;873&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceQM0U/dJMcaheiZSq/3YPqFKAEKgPFyck7ma9KZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceQM0U/dJMcaheiZSq/3YPqFKAEKgPFyck7ma9KZK/img.png&quot; data-alt=&quot;실제로 클로드가 보낸 이메일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceQM0U/dJMcaheiZSq/3YPqFKAEKgPFyck7ma9KZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceQM0U%2FdJMcaheiZSq%2F3YPqFKAEKgPFyck7ma9KZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1842&quot; height=&quot;873&quot; data-origin-width=&quot;1842&quot; data-origin-height=&quot;873&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 style=&quot;text-align: justify;&quot; data-end=&quot;412&quot; data-start=&quot;270&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;리포트에는 신규 지원 현황, 진행 중인 전형, 합격&amp;middot;탈락 여부, 확인이 필요한 일정 등이 정리되어 있다. 실제 사용 중에는 마감이 얼마 남지 않은 지원 건을 리마인드로 확인하거나, 이전에 확인하지 못했던 서류 결과를 리포트를 통해 발견하는 경우도 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;412&quot; data-start=&quot;270&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;470&quot; data-start=&quot;414&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;가장 크게 달라진 점은 &lt;b&gt;취업 지원 현황을 관리하는 데 드는 신경 쓰는 비용이 줄었다는 것&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;632&quot; data-start=&quot;472&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이전에는 주기적으로 Notion DB를 열어 전체 지원 현황을 직접 확인하고, 오래 대기 중인 항목을 찾아 상태를 수정해야 했다. 이제는 리포트를 통해 현재 상황을 한 번에 확인하고, 30일 이상 무응답인 항목도 자동으로 정리되기 때문에 DB 상태를 항상 최신으로 유지할 수 있게 됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;632&quot; data-start=&quot;472&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;733&quot; data-start=&quot;634&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;취업 준비 과정에서 지원 자체뿐 아니라 지원 현황을 관리하는 일도 생각보다 많은 에너지를 필요로 한다는 것을 느꼈는데, 이 부분을 자동화하면서 관리에 대한 부담이 확실히 줄었다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;325&quot; data-start=&quot;281&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;취업 준비 과정에서도 개발자가 문제를 해결하는 방식은 적용할 수 있다고 느꼈다. 반복적으로 발생하는 불편함을 발견하고, 필요한 기능을 정의하고, 이를 자동화된 흐름으로 바꾸는 과정은 개발 과정과 크게 다르지 않았다. 특히 이번 경험에서 중요했던 것은 Claude가 코드를 작성해주는 것이 아니라, 내가 원하는 결과를 구체적인 요구사항으로 정의하는 일이었다. &quot;어떤 데이터를 가져올지&quot;, &quot;어떤 상황을 리마인드할지&quot;, &quot;예외 상황은 어떻게 처리할지&quot;를 정리하면서 자연스럽게 서비스의 요구사항을 설계하는 경험을 할 수 있었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Claude</category>
      <category>Claude Cowork</category>
      <category>자동화</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/413</guid>
      <comments>https://3uomlkh.tistory.com/413#entry413comment</comments>
      <pubDate>Fri, 31 Jul 2026 17:26:20 +0900</pubDate>
    </item>
    <item>
      <title>Git 기본 브랜치를 master에서 main으로 변경하기</title>
      <link>https://3uomlkh.tistory.com/412</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;새 프로젝트를 만들 때마다 기본 브랜치가 master로 생성되는 게 불편하다면, 전역 설정 한 번으로 해결할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;설정해두면 이후 생성하는 모든 저장소에 자동으로 적용된다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;터미널에서 아래 명령어를 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global init.defaultBranch main
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;아래 명령어로 현재 설정값을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global --get init.defaultBranch
&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;정상적으로 설정되었다면 main이 출력된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;main
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;새 디렉터리를 만들고 Git 저장소를 초기화해서 직접 확인해볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;mkdir test-git
cd test-git
git init
git branch
&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;main 브랜치로 초기화된 것을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이미 master로 생성된 저장소라면?&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기존 저장소는 자동으로 변경되지 않으므로 직접 이름을 바꿔야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;git branch -m master main
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;원격 저장소가 연결되어 있다면 변경된 브랜치를 푸시한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;git push -u origin main
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>TIL</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/412</guid>
      <comments>https://3uomlkh.tistory.com/412#entry412comment</comments>
      <pubDate>Mon, 8 Jun 2026 23:56:54 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 데이터 무결성과 제약 조건 종류</title>
      <link>https://3uomlkh.tistory.com/411</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;인프런 &amp;lt;김영한의 실전 데이터베이스 - 기본편&amp;gt; 강의를 토대로 정리한 내용입니다.&lt;/span&gt;&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;style7&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;- 목차 -&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1. 데이터 무결성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;2. 기본 제약 조건&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;1) NOT NULL&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;2) UNIQUE&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;3) PRIMARY KEY&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;4) DEFAULT&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;3. 외래 키 제약 조건&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;1) 규칙&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;2) ON DELETE / ON UPDATE 옵션&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;4. CHECK 제약 조건&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;데이터 무결성&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;데이터 무결성(Data Integrity)&lt;/b&gt;&amp;nbsp;: 데이터의 정확성, 일관성, 유효성을 유지하는 성질&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;제약 조건(Constraints)&lt;/b&gt;&amp;nbsp;: 데이터 무결성을 강제하기 위해 테이블의 특정 컬럼에 설정하는 규칙&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;GIGO (Garbage In, Garbage Out)&lt;/b&gt; : 쓰레기가 들어가면 쓰레기가 나온다. 초기 데이터의 신뢰도가 결과물의 신뢰도를 결정한다.&lt;/span&gt;&lt;/p&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;잘못된 데이터는 잘못된 비즈니스 결정, 시스템 오류, 데이터 불일치 등 심각한 문제를 야기한다. 그래서 데이터베이스는 데이터를 지키는 &lt;b&gt;'최후의 보루'&lt;/b&gt; 역할을 해야 한다. 1차 검증은 애플리케이션에서 하더라도, 데이터베이스에 유효한 값만 입력되도록 강제하기 위해 &lt;b&gt;제약 조건(Constraints)&lt;/b&gt; 이 필요하다. 제약 조건에는 기본 제약 조건, 외래 키 제약 조건, CHECK 제약 조건이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기본 제약 조건&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;제약조건&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특징&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;NOT NULL&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;NULL 값 방지&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;필수 정보 누락을 원천 차단&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;UNIQUE&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;중복 값 방지&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;모든 값이 테이블 내에서 고유해야 함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;PRIMARY KEY&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;행의 고유 식별자&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;NOT NULL + UNIQUE, 테이블당 1개만 설정 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DEFAULT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기본값 설정&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;값을 입력하지 않았을 때 자동으로 채워질 값 지정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;NOT NULL : NULL 값 방지&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;NOT NULL은 해당 컬럼에 NULL 값이 저장되는 것을 허용하지 않는다. 필요한 정보가 누락되는 것을 막는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;email VARCHAR(255) NOT NULL&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;UNIQUE : 중복 값 방지&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;UNIQUE는 해당 컬럼에 들어가는 모든 값은 테이블 내에서 반드시 고유해야 한다. 중복 데이터가 쌓이는 것을 막는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;email VARCHAR(255) UNIQUE&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;PRIMARY KEY (기본 키) : 행의 대표 식별자&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 테이블의 각 행을 고유하게 식별하는 단 하나의 대표키다. NOT NULL + UNIQUE 특징을 모두 가지며, 테이블당 하나만 설정할 수 있다. 기본 키를 기준으로 데이터가 저장되고 검색되므로 성능에도 매우 중요한 역할을 한다. &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;user_id BIGINT PRIMARY KEY
&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;여러 컬럼을 묶어 복합 기본 키로 지정할 수도 있다. &lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE order_items (
  order_id INT,
  item_id  INT,
  quantity INT,
  PRIMARY KEY (order_id, item_id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DEFAULT : 기본 값 설정&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DEFAULT는 특정 컬럼에 값을 명시적으로 입력하지 않았을 경우, 자동으로 설정될 기본값을 지정한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;status VARCHAR(50) DEFAULT 'PENDING'
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;외래 키 제약 조건 (Foreign Key) &lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;참조 무결성(Referential Integrity) : 두 테이블의 관계가 항상 유효하고 일관된 상태를 유지해야 한다는 원칙,&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;규칙&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 자식 테이블&lt;/b&gt;: 부모 테이블에 존재하지 않는 id 값을 자식 테이블의 FK 컬럼에 넣으려는 시도를 막는다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 부모 테이블&lt;/b&gt;: 자식 테이블에서 참조하고 있는 행을 부모 테이블에서 함부로 삭제/변경하지 못하게 막는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE orders (
  id      INT PRIMARY KEY,
  user_id INT,
  FOREIGN KEY (user_id) REFERENCES users(id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ON DELETE / ON UPDATE 옵션&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;외래 키의 ON DELETE와 ON UPDATE 에서 별도의 참조 동작을 지정하지 않으면, 기본적으로 부모 데이터의 삭제나 수정을 제한 한다. 지정할 수 있는 참조 동작은 다음과 같다. &lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;옵션&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;동작&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RESTRICT (기본값)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자식 테이블에 참조하는 행이 있으면 부모 행을 삭제/수정할 수 없다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CASCADE&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;부모 행이 삭제/수정되면 자식 행도 함께 자동으로 삭제/수정된다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;SET NULL&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;부모 행이 삭제/수정되면 자식의 FK 컬럼 값을 NULL로 설정한다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;SET DEFAULT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;부모 행이 삭제/수정되면 자식의 FK 컬럼 값을 기본값으로 변경한다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;NO ACTION&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉각 체크하지 않고, 트랜잭션 종료 시 무결성을 검사한다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;  CASCADE는 의도치 않은 대량 삭제 위험이 있다. 실무에서는 애플리케이션 계층에서 명시적으로 처리하는 방식을 선호하기도 한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;CHECK 제약 조건&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; CHECK는 컬럼에 들어올 값의 범위나 조건을 직접 지정하며, 조건식이 참(true) 일 때만 입력을 허용한다. 데이터의 '내용'과 '비즈니스 규칙'을 DB 레벨에서 강제하기 위해 사용한다. &lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE products (
  id            INT PRIMARY KEY,
  name          VARCHAR(100) NOT NULL,
  price         INT CHECK (price &amp;gt;= 0),
  discount_rate INT CHECK (discount_rate BETWEEN 0 AND 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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;컬럼 간 비교 조건도 표현할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE employees (
  id         INT PRIMARY KEY,
  hire_date  DATE,
  birth_date DATE,
  CHECK (hire_date &amp;gt; birth_date)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;⚠️ &lt;b&gt;MySQL 주의사항&lt;/b&gt;: MySQL 8.0 이전 버전에서는 CHECK 제약 조건을 파싱은 하지만 실제로 강제하지 않는다. MySQL 8.0+, PostgreSQL, Oracle 등에서는 정상적으로 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;  최근에는 유연성과 테스트 편의성을 위해 애플리케이션 코드에서 검증을 처리하는 추세다. DB의 CHECK 제약 조건은 핵심 데이터의 '최후 방어선'으로만 신중하게 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>DB</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/411</guid>
      <comments>https://3uomlkh.tistory.com/411#entry411comment</comments>
      <pubDate>Sat, 11 Apr 2026 15:41:12 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 인덱스 생성 &amp;middot; 조회 &amp;middot; 삭제 및 실행 계획 보기(+ 인덱스로 정렬 작업 성능 개선하기)</title>
      <link>https://3uomlkh.tistory.com/410</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;인프런 &amp;lt;김영한의 실전 데이터베이스 - 기본편&amp;gt; 강의를 토대로 정리한 내용입니다.&lt;/i&gt;&lt;/span&gt;&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;style7&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;- 목차 -&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1. 인덱스란?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;2. 인덱스 사용하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;1) 인덱스 생성, 조회, 삭제&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;2) 인덱스 실행 계획 보기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;3. 인덱스가 사용되는 상황&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;1) 동등 비교&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;2) 범위 검색&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; 3) LIKE 범위 검색&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;4. 인덱스로 ORDER BY 성능 개선하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스란 &lt;span style=&quot;text-align: left;&quot;&gt;&lt;b&gt;특정 컬럼(들)의 데이터를 기반으로 생성되는, 원본 테이블과는 별개의 특수한 자료구조&lt;/b&gt;이다. 인덱스를 사용하면 풀 테이블 스캔을 사용하지 않아서 빠른 검색이 가능하다. 그 이유는 인덱스 내부의 데이터는 &lt;b&gt;항상 정렬된 상태를 유지&lt;/b&gt;하기 때문이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;  &lt;b&gt;풀 테이블 스캔(Full Table Scan)&lt;/b&gt;이란?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;풀 테이블 스캔은 말 그대로 테이블을 처음부터 끝까지 탐색한다는 뜻이다. 따라서 데이터가 많으면 많을수록 검색 시간이 정비례해서 늘어난다. 찾으려는 데이터가 우연히 첫 번째 행에 있다면 한 번의 비교만으로 끝나겠지만, 맨 마지막 행에 있다면 데이터의 수만큼 비교를 해야하는 상황이 된다. 이런 풀 테이블 스캔은 느린 검색의 원인이 되고, 매우 비용이 높은 작업이므로 서비스의 핵심 기능에서 가급적 풀테이블 스캔이 발생하지 않도록 설계해야 한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;만약 item_name 컬럼에 인덱스를 생성하고 다음 쿼리를 실행하면 데이터베이스는 어떻게 동작할까?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768634804599&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM items WHERE item_name = '게이밍 노트북';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;풀 테이블 스캔 대신, item_name 기반의 인덱스를 찾아간다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;정렬되어 있기 때문에 '게이밍 노트북'이라는 값을 빠르게 찾아낸다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;찾아낸 인덱스 항목에서 실제 데이터의 행 위치 값을 확인한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스는 &lt;b&gt;트리 자료 구조&lt;/b&gt;를 사용하기 때문에 이런 빠른 검색이 가능한데, 이전 발행 글 중 이진 탐색 트리에 대해 정리한 글이 있어 첨부한다. 핵심은 &lt;b&gt;데이터를 입력하는 시점에 정렬해서 보관&lt;/b&gt;한다는 점이다. 따라서 검색 시에도 찾으려는 데이터보다 작은 값은 왼쪽, 큰 값은 오른쪽으로 가면서 빠르게 찾아낼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1768635342033&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;[CS 발표_07] 완전 이진 트리, 포화 이진 트리, 이진 탐색 트리란?&quot; data-og-description=&quot;완전 이진 트리, 포화 이진 트리, 이진 탐색 트리의 차이점은 무엇인가요? &amp;nbsp;발표 때 사용한 PDF로 질문에 대한 답변을 대신합니다:)&amp;nbsp;더보기기술 면접 대비 CS 전공 핵심요약집 | 이수진 - 교보문고&quot; data-og-host=&quot;3uomlkh.tistory.com&quot; data-og-source-url=&quot;https://3uomlkh.tistory.com/294&quot; data-og-url=&quot;https://3uomlkh.tistory.com/294&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bf8qJO/dJMb9lk0OPQ/pRtMbdwfGQVhxRWoIg8Sb1/img.png?width=800&amp;amp;height=494&amp;amp;face=0_0_800_494,https://scrap.kakaocdn.net/dn/dcIlLb/dJMb9hCUXTt/gXaKM0J5yR3PoKZgEJw201/img.png?width=800&amp;amp;height=494&amp;amp;face=0_0_800_494,https://scrap.kakaocdn.net/dn/AB6vB/dJMb81fMgjW/7tRnniKSScNIzOcGnCunoK/img.png?width=949&amp;amp;height=589&amp;amp;face=0_0_949_589&quot;&gt;&lt;a href=&quot;https://3uomlkh.tistory.com/294&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://3uomlkh.tistory.com/294&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bf8qJO/dJMb9lk0OPQ/pRtMbdwfGQVhxRWoIg8Sb1/img.png?width=800&amp;amp;height=494&amp;amp;face=0_0_800_494,https://scrap.kakaocdn.net/dn/dcIlLb/dJMb9hCUXTt/gXaKM0J5yR3PoKZgEJw201/img.png?width=800&amp;amp;height=494&amp;amp;face=0_0_800_494,https://scrap.kakaocdn.net/dn/AB6vB/dJMb81fMgjW/7tRnniKSScNIzOcGnCunoK/img.png?width=949&amp;amp;height=589&amp;amp;face=0_0_949_589');&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;[CS 발표_07] 완전 이진 트리, 포화 이진 트리, 이진 탐색 트리란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;완전 이진 트리, 포화 이진 트리, 이진 탐색 트리의 차이점은 무엇인가요? &amp;nbsp;발표 때 사용한 PDF로 질문에 대한 답변을 대신합니다:)&amp;nbsp;더보기기술 면접 대비 CS 전공 핵심요약집 | 이수진 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;3uomlkh.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;인덱스 사용하기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스를 생성, 조회, 삭제하는 명령어는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768634127282&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX 인덱스이름 ON 테이블이름(컬럼1, 컬럼2, ...);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1768634139476&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW INDEX FROM 테이블이름;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1768634148690&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DROP INDEX 인덱스이름 ON 테이블이름;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;아래와 같이 인덱스를 만들었을 때, 해당 인덱스가 진짜 사용되고 있는지 어떻게 알 수 있을까?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769239120312&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_items_item_name ON items (item_name);&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스를 만들었다고 무조건 그 인덱스가 사용되는 것은 아니다. 쿼리 옵티마이저가 풀 테이블 스캔이 더 빠르겠다고 판단하면 사용되지 않을 수도 있다. 인덱스가 정말 사용되는지 확인하려면 다음과 같이 EXPLAIN 명령어를 사용하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768634220689&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN SELECT * FROM items WHERE item_name = '게이밍 노트북';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;898&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xlWZi/dJMcafFl6ko/veKOm8jKGk9m2WBxCmhBvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xlWZi/dJMcafFl6ko/veKOm8jKGk9m2WBxCmhBvK/img.png&quot; data-alt=&quot;인덱스가 사용되고 있지 않을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xlWZi/dJMcafFl6ko/veKOm8jKGk9m2WBxCmhBvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxlWZi%2FdJMcafFl6ko%2FveKOm8jKGk9m2WBxCmhBvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;898&quot; height=&quot;216&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인덱스가 사용되고 있지 않을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;type : ALL&lt;/b&gt; 은 풀 테이블 스캔을 의미한다. 인덱스를 제대로 사용했다면 ref, range 등 다른 값이 표시된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;key : NULL&lt;/b&gt; 은 쿼리를 실행할 때 어떤 인덱스도 사용하지 못했다는 뜻이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;rows : 25&lt;/b&gt; 는 옵티마이저가 쿼리를 처리하기 위해 탐색할 것으로 예측하는 행의 수이다. 현재 item 테이블의 전체 데이터가 25개이므로, 모든 행을 탐색할 것으로 예측하고 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;filtered : 10.00&lt;/b&gt; 은 25개의 행을 모두 읽고 그중 10%인 2.5개 정도의 행이 WHERE절 조건을 만족할 것이라고 예측하고 있음을 의미한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Extra : Using where&lt;/b&gt; 은 데이터를 가져온 후에 WHERE절의 조건을 사용해 필터링 작업을 수행했다는 의미다. 인덱스를 사용하면 NULL이 나온다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음은 idx_items_item_name 이라는 인덱스를 만든 후 실행한 결과다. 항목 결과가 꽤 달라진 것을 볼 수 있다.&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;893&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n6nKC/dJMb996cPDu/tby3QmxEwFUjVsKGCFWpA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n6nKC/dJMb996cPDu/tby3QmxEwFUjVsKGCFWpA0/img.png&quot; data-alt=&quot;인덱스가 사용될 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n6nKC/dJMb996cPDu/tby3QmxEwFUjVsKGCFWpA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn6nKC%2FdJMb996cPDu%2Ftby3QmxEwFUjVsKGCFWpA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;893&quot; height=&quot;206&quot; data-origin-width=&quot;893&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인덱스가 사용될 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;type : ref&lt;/b&gt; 는 = 조건이나 JOIN에서 인덱스를 사용했다는 의미다. 만약 &lt;b&gt;range&lt;/b&gt;라면 범위 검색(BETWEEN, &amp;gt;, &amp;lt;, &amp;gt;= 등)에서 인덱스를 사용했다는 의미다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;key : idx_items_item_name&lt;/b&gt; 는 쿼리 실행에 &lt;span style=&quot;text-align: start;&quot;&gt;idx_items_item_name 인덱스가 사용되었음을 명확히 보여준다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;rows : 1&lt;/b&gt; 을 보면, 옵티마이저가 쿼리를 처리하기 위해 단 한 개의 행만 탐색할 것으로 예측하고 있다. 이 값이 작을수록 효율적인 쿼리라고 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;filtered : 100.00&lt;/b&gt; 은 인덱스를 통해서 스캔한 1개의 행을 100% 선택한다는 뜻이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Extra : NULL&lt;/b&gt; 은 데이터를 가져온 후 별도의 필터링 작업이 필요 없다는 뜻이다. &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;동등 비교의 경우, 위에서 &lt;span style=&quot;text-align: start;&quot;&gt;EXPLAIN&amp;nbsp;&lt;/span&gt; 명령어를 설명할 때 함께 설명했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;type이 ref 라면 인덱스를 사용해 동등 비교(=) 조건으로 데이터를 찾았다는 의미다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;범위 검색 역시 비슷한데, type이 range 라면 범위 검색(BETWEEN , &amp;gt; , &amp;lt; , LIKE 등)에 인덱스가 사용된다는 의미다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;먼저 인덱스 없이 다음 EXPLAIN 명령어를 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769239060581&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN SELECT * FROM items WHERE price BETWEEN 50000 AND 100000;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1018&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kYQA7/dJMcagxwDRG/zliLxFakpnk3lowKnxNeJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kYQA7/dJMcagxwDRG/zliLxFakpnk3lowKnxNeJ0/img.png&quot; data-alt=&quot;범위 검색에서 인덱스가 사용되고 있지 않을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kYQA7/dJMcagxwDRG/zliLxFakpnk3lowKnxNeJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkYQA7%2FdJMcagxwDRG%2FzliLxFakpnk3lowKnxNeJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1018&quot; height=&quot;204&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;범위 검색에서 인덱스가 사용되고 있지 않을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;b&gt;type : ALL&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt; 을 보면 풀 테이블 스캔이 발생한 것을 알 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;key : NULL&lt;/b&gt; 은 price 컬럼을 조건으로 사용했지만, 해당 컬럼에 인덱스가 없어 인덱스를 사용하지 않았다는 의미다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;rows : 25&lt;/b&gt; 는 테이블의 전체 행 수를 예측한 수치로, 풀 테이블 스캔을 했기 때문이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;filtered : 11.11&lt;/b&gt; 은 전체 행 중에 WHERE price BETWEEN 50000 AND 100000 조건을 만족하는 행은 약 11.11% 일 것이라고 예측한다는 의미다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;Extra : Using where&lt;/b&gt; 은 데이터를 가져온 후에 WHERE 절의 조건을 사용해 필터링했다는 의미다. 인덱스가 없기 때문에 모든 데이터를 다 가져온 후, 조건에 맞는지 일일이 비교하는 방식으로 골라내고 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위와 같이 조건절에 사용되는 컬럼에 인덱스가 없으면 풀 테이블 스캔을 피할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음은 idx_items_price 인덱스를 만들어 다시 실행 계획을 보자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769239405805&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_items_price ON items (price);&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1016&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d454Hb/dJMcaiPBGOG/ZIhkng0akubI6P11T1o6Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d454Hb/dJMcaiPBGOG/ZIhkng0akubI6P11T1o6Qk/img.png&quot; data-alt=&quot;범위 검색에서 인덱스가 사용되고 있을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d454Hb/dJMcaiPBGOG/ZIhkng0akubI6P11T1o6Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd454Hb%2FdJMcaiPBGOG%2FZIhkng0akubI6P11T1o6Qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1016&quot; height=&quot;203&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;범위 검색에서 인덱스가 사용되고 있을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;type : range&lt;/b&gt; 은 인덱스를 사용해 특정 범위의 데이터를 스캔했다는 의미다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;key : idx_items_price&lt;/b&gt; 는 쿼리 실행에 idx_items_price 인덱스가 사용되었다는 의미다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;rows : 5&lt;/b&gt; 를 보면 기존 25에서 5로 확 줄어들었는데, 옵티마이저가 인덱스를 통해 조건에 맞는 5개의 데이터만 스캔할 것이라고 판단하고, 그 행 수를 5개로 예측한 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;filtered : 100.00&lt;/b&gt; 는 인덱스를 통해서 스캔한 5개의 행을 100% 선택한다는 의미다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Extra : Using index condition&lt;/b&gt; 는 인덱스 정보만으로 WHERE 조건절을 최대한 필터링한 후, 조건을 만족하는 데이터의 전체 행만 가져왔다는 의미다.&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;1014&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DoqmC/dJMcajuc7iY/gVkA5jukAAmhC5HJQW2Jm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DoqmC/dJMcajuc7iY/gVkA5jukAAmhC5HJQW2Jm1/img.png&quot; data-alt=&quot;실제 조회 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DoqmC/dJMcajuc7iY/gVkA5jukAAmhC5HJQW2Jm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDoqmC%2FdJMcajuc7iY%2FgVkA5jukAAmhC5HJQW2Jm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1014&quot; height=&quot;211&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;211&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스를 사용한 실제 결과를 보면 idx_items_price 인덱스를 사용한 후에는 &lt;b&gt;인덱스 키인 price를 기준으로 정렬&lt;/b&gt;되었다는 것을 알 수 있다. 이처럼 인덱스를 사용하면 쿼리 결과의 정렬 순서가 달라질 수 있다. 그러나 이런 순서가 항상 보장되는 것은 아니므로, 정렬이 필요하면 ORDER BY 절을 추가하는 것이 권장된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;LIKE 범위 검색&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;LIKE 절에서 인덱스 사용 시, 와일드카드(%)의 위치가 중요하다. &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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;WHERE item_name LIKE '게이밍%' : 인덱스 사용 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;WHERE item_name LIKE '%게이밍' : 인덱스 사용 불가 &amp;rarr; 풀 테이블 스캔 발생&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;WHERE item_name LIKE '%게이밍%' : 인덱스 사용 불가 &amp;rarr; 풀 테이블 스캔 발생 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;인덱스로 ORDER BY 성능 개선하기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스는 정렬된 자료구조이기 때문에, 인덱스를 잘 활용한다면 정렬(ORDER BY) 작업의 성능을 개선할 수 있다. 데이터베이스는 이 과정에서 filesort라는 별도의 정렬 작업을 생략할 수 있게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;  filesort란?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;메모리나 디스크를 사용해 정렬하는 내부 프로세스를 의미한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스를 활용하지 않은 다음 쿼리의 실행 계획을 보면 filesort를 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769240896164&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN SELECT * FROM items ORDER BY stock_quantity;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1017&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boAVbn/dJMcahQKo5L/JtZUZ7TNqQZQHrKTxneUu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boAVbn/dJMcahQKo5L/JtZUZ7TNqQZQHrKTxneUu0/img.png&quot; data-alt=&quot;Extra : filesort인 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boAVbn/dJMcahQKo5L/JtZUZ7TNqQZQHrKTxneUu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboAVbn%2FdJMcahQKo5L%2FJtZUZ7TNqQZQHrKTxneUu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1017&quot; height=&quot;209&quot; data-origin-width=&quot;1017&quot; data-origin-height=&quot;209&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Extra : filesort인 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Extra : filesort&lt;/b&gt; 는 데이터를 모두 찾을 후에 stock_quantity를 기준으로 정렬 작업이 추가되었음을 보여준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;WHERE절과 ORDER BY절의 정렬 기준이 같으면 인덱스를 스캔하는 것만으로도 검색과 정렬을 한번에 해결할 수 있다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;price 컬럼에 idx_items_price 컬럼을 만들어 둔 상태에서, 다음 쿼리를 실행해보자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769241376246&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN SELECT * FROM items WHERE price BETWEEN 50000 AND 100000 ORDER BY price;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1012&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9uvCf/dJMcagxwEnF/wMEnimlWJJ6SDknpkBbowk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9uvCf/dJMcagxwEnF/wMEnimlWJJ6SDknpkBbowk/img.png&quot; data-alt=&quot;인덱스를 사용해 정렬까지 한 번에 처리한 경우아&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9uvCf/dJMcagxwEnF/wMEnimlWJJ6SDknpkBbowk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9uvCf%2FdJMcagxwEnF%2FwMEnimlWJJ6SDknpkBbowk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1012&quot; height=&quot;202&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;202&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;아까와 달리 Using filesort가 없어지고 Using index condition으로 바뀌었다. 데이터베이스 옵티마이저는 idx_items_price가 이미 price 순서로 정렬되어 있다는 것을 알고 있어서, 조건에 맞게 스캔만 해도 자연스럽게 price 순서로 정렬된 결과를 얻을 수 있다. 따라서 WHERE절과 ORDER BY절이 동일한 인덱스를 효율적으로 사용할 수 있다면, 데이터베이스는 정렬을 생략하고, 가장 빠른 방식으로 쿼리를 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터베이스 옵티마이저는 &lt;b&gt;역방향 스캔(Backward Index Scan)이 가능&lt;/b&gt;하다. 따라서 오름차순 정렬이 아닌 내림차순 정렬을 하더라도 단일 컬럼 인덱스에서는 filesort 없이 처리할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769242125391&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN SELECT * FROM items WHERE price BETWEEN 50000 AND 100000 ORDER BY price DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1019&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMm3gQ/dJMcafyC35W/w15cMOTWZHLxmgYQKeLg80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMm3gQ/dJMcafyC35W/w15cMOTWZHLxmgYQKeLg80/img.png&quot; data-alt=&quot;역방향 스캔을 한 실행 계획&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMm3gQ/dJMcafyC35W/w15cMOTWZHLxmgYQKeLg80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMm3gQ%2FdJMcafyC35W%2Fw15cMOTWZHLxmgYQKeLg80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1019&quot; height=&quot;207&quot; data-origin-width=&quot;1019&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;역방향 스캔을 한 실행 계획&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Extra : Using index; Backward index scan&lt;/b&gt; 은 옵티마이저가 인덱스를 역순으로 스캔했음을 의미한다. 인덱스는 양방향 탐색이 가능하기 때문에 WHERE 조건에 맞는 데이터를 찾는 과정에서 이미 정렬 순서가 만족되므로 별도의 filesort 작업이 필요없다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;MySQL 8.0 버전부터는 내림차순 인덱스(Descending Index)를 정식으로 지원하기 때문에, 정렬 방향과 일치하는 인덱스를 직접 만들 수 있다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기존 인덱스를 삭제하고, 다음과 같이 내림차순 인덱스를 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769242321277&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_items_price_desc ON items (price DESC);&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;결과는 다음과 같다. Extra 컬럼에서 Backward index scan이 사라진 것을 볼 수 있다. 이제 역방향 스캔 없이 정방향 스캔만하면 된다. 미세한 차이지만, 역방향 스캔이 정방향 스캔보다 좀 더 빠르기 때문에 이렇게 하면 가장 최적화된 버전이라고 할 수 있다.&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;1027&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clyDNY/dJMcaf6sway/9k3jVP9xtrV62MNKELSYLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clyDNY/dJMcaf6sway/9k3jVP9xtrV62MNKELSYLK/img.png&quot; data-alt=&quot;내림차순 인덱스 적용 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clyDNY/dJMcaf6sway/9k3jVP9xtrV62MNKELSYLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclyDNY%2FdJMcaf6sway%2F9k3jVP9xtrV62MNKELSYLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1027&quot; height=&quot;212&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;212&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;</description>
      <category>DB</category>
      <category>Database</category>
      <category>index</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/410</guid>
      <comments>https://3uomlkh.tistory.com/410#entry410comment</comments>
      <pubDate>Sat, 24 Jan 2026 17:19:29 +0900</pubDate>
    </item>
    <item>
      <title>[DB] UNION, UNION ALL, UNION 정렬</title>
      <link>https://3uomlkh.tistory.com/409</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;i&gt; 인프런 &amp;lt;김영한의 실전 데이터베이스 - 기본편&amp;gt; 강의를 토대로 정리한 내용입니다.&lt;br /&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #24292f; text-align: left;&quot; 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;h2 style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;UNION&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;UNION은 여러 개의 결과 집합을 아래로(수직으로) 이어 붙여서 더 많은 행을 가진 집합으로 만드는 기술이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc; color: #24292f; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;UNION으로 연결되는 모든 SELECT문은&amp;nbsp;&lt;b&gt;컬럼의 개수가 동일&lt;/b&gt;해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;각 SELECT 문의 같은 위치에 있는 컬럼들은&amp;nbsp;&lt;b&gt;서로 호환 가능한 데이터 타입&lt;/b&gt;이어야 한다. (이름은 달라도 됨)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최종 결과의 컬럼 이름은 첫번째 SELECT 문의 컬럼 이름을 따른다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;중복되는 행은 자동으로 제거한다.&lt;/b&gt; &amp;rarr; UNION ALL을 사용하면 의도적으로 중복을 허용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아래 예시를 보면, 활동 고객 명단(users)과 탈퇴 고객 명단(retired_users)을 합쳤다. UNION은 기본적으로 두 결과 집합을 합친 뒤, 완전히 중복되는 행은 자동으로 제거하여 고유한 값만 남긴다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767415997108&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select name, email from users

union

select name, email from retired_users;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;UNION ALL&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; UNION ALL은 여러 개의 결과 집합을 아래로(수직으로) 이어 붙여 하나의 결과 집합으로 만든다. 중복 행을 제거하지 않고, 모든 행을 그대로 포함한다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;두 종류의 고객에게 이메일을 보내려고 할 때, 첫 번째 그룹은 '전자기기' 카테고리의 상품을 구매한 이력이 있는 고객이고, 두 번째 그룹은 '서울'에 거주하는 고객이다. '서울'에 살면서 '전자기기'를 구매한 고객을 최종 목록에 한 번만 포함할지, 중복을 허용할지에 따라 UNION을 쓸지, UNION ALL을 쓸지 결정할 수 있다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;먼저, UNION을 사용한 결과는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767416572873&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 전자기기 구매 고객
select u.name, u.email
from users u
inner join orders o on u.user_id = o.user_id
inner join products p on o.product_id = p.product_id
where p.category = '전자기기'

union

-- 서울 거주 고객
select name, email
from users
where address like '서울%';&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;211&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQSg7x/dJMcaihGLtB/gNNskRK2Gj49sGQvjKM8T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQSg7x/dJMcaihGLtB/gNNskRK2Gj49sGQvjKM8T0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQSg7x/dJMcaihGLtB/gNNskRK2Gj49sGQvjKM8T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQSg7x%2FdJMcaihGLtB%2FgNNskRK2Gj49sGQvjKM8T0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;211&quot; height=&quot;114&quot; data-origin-width=&quot;211&quot; data-origin-height=&quot;114&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;UNION ALL을 사용하면 '션', '네이트', '마리퀴리'가 중복되어 나타난다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767416738406&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 전자기기 구매 고객
select u.name, u.email
from users u
inner join orders o on u.user_id = o.user_id
inner join products p on o.product_id = p.product_id
where p.category = '전자기기'

union all

-- 서울 거주 고객
select name, email
from users
where address like '서울%';&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;209&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cn1Eb2/dJMcagEb1hE/uH062GkYOjbgWLUHkZDQoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cn1Eb2/dJMcagEb1hE/uH062GkYOjbgWLUHkZDQoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cn1Eb2/dJMcagEb1hE/uH062GkYOjbgWLUHkZDQoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcn1Eb2%2FdJMcagEb1hE%2FuH062GkYOjbgWLUHkZDQoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;209&quot; height=&quot;159&quot; data-origin-width=&quot;209&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;UNION vs UNION ALL&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;한 줄 요약 : UNION ALL이 훨씬 빠르다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc; color: #24292f; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;UNION&amp;nbsp;&lt;/b&gt;: 두 결과 집합을 합친 후,중복된 행을 제거한다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;UNION ALL&lt;/b&gt;&amp;nbsp;: 중복 제거 과정 없이, 두 결과 집합을 그대로 모두 합친다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;중복을 제거하는 추가 과정이 없는 &lt;b&gt;UNION ALL이 훨씬 빠르다. &lt;/b&gt;따라서 중복을 제거해야만 하는 명확한 요구사항이 있을 때만 UNION을 사용한다. 불필요한 UNION은 쿼리를 느리게 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;UNOIN 정렬&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; ORDER BY 절은 전체 UNION 연산의 가장 마지막에 한 번만 사용해야 한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 합쳐진 최종 결과 전체에 대한 순서를 결정해야 하기 때문에, ORBER BY 절의 위치가 중요하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다음과 같이 활동 고객과 탈퇴 고객의 명단을 합친 후 이름(name) 기준 오름차순 정렬을 하면, ORDER BY 절을 UNION 연산자의 마지막에 배치함으로써, 합쳐진 모든 고객의 이름이 글자 순서대로 정렬된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;background-color: #f6f8fa; color: #24292f; text-align: start;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select name, email from users

union

select name, email from retired_users
order by name; -- 최종 결과에 대한 정렬&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lz9ew/dJMb99SAkPj/w53EMcQEk0gZz7ZO4sYrv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lz9ew/dJMb99SAkPj/w53EMcQEk0gZz7ZO4sYrv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lz9ew/dJMb99SAkPj/w53EMcQEk0gZz7ZO4sYrv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flz9ew%2FdJMb99SAkPj%2Fw53EMcQEk0gZz7ZO4sYrv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;270&quot; height=&quot;152&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또한 각 테이블에 이름이 다른 컬럼이 있다면, 그 컬럼들을 아우를 수 있는 중립적인 별칭을 사용하는 것을 권장한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;background-color: #f6f8fa; color: #24292f; text-align: start;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select distinct u.name as 고객명, u.email as 이메일
from users u
join orders o on u.user_id = o.user_id
join products p on o.product_id = p.product_id
where p.category = '전자기기'

union all

select distinct u.name as 고객명, u.email as 이메일
from users u
join orders o on u.user_id = o.user_id
where o.quantity &amp;gt; 1;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DB</category>
      <category>SQL</category>
      <category>union</category>
      <category>union all</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/409</guid>
      <comments>https://3uomlkh.tistory.com/409#entry409comment</comments>
      <pubDate>Sat, 3 Jan 2026 14:38:24 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 내부 조인, 외부조인, 셀프조인, 크로스조인</title>
      <link>https://3uomlkh.tistory.com/408</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt; 인프런 &amp;lt;김영한의 실전 데이터베이스 - 기본편&amp;gt; 강의를 토대로 정리한 내용입니다.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #24292f; text-align: left;&quot; 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 style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;만약 모든 정보를 한 테이블에 다 넣는다면 조회할 때는 좀 더 편리할 수도 있겠지만, 다음과 같은 문제가 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #24292f; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터 중복&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;갱신 이상&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;삽입 이상&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;삭제 이상&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이러한 문제들 때문에 데이터베이스를 설계할 때는 &lt;b&gt;정규화(Normalization)&lt;/b&gt;라는 과정을 거친다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; 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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;정규화(Normalization)&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 style=&quot;color: #24292f; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그런데 이렇게 정규화를 거쳐서 쪼개진 테이블들에서 의미 있는 정보를 얻으려면 이 흩어진 데이터 조각들을 다시 합쳐야 한다. 이때 사용하는 기술이 바로 &lt;b&gt;조인(JOIN)&lt;/b&gt; 이다. 조인의 종류에는 &lt;b&gt;내부 조인, 외부 조인, 셀프 조인, 크로스 조인&lt;/b&gt;이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; 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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;조인(JOIN)&lt;/b&gt; : 두 개 이상의 테이블을 특정 컬럼을 기준으로 연결하여, 마치 처음부터 하나의 테이블이었던 것처럼 보여주는 기능.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내부 조인&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;양쪽 테이블에 모두 공통으로 존재하는 데이터만을 보고싶을 때 사용하는 기술.&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1770444933507&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT 컬럼1, 컬럼2, ...
FROM 테이블A
INNER JOIN 테이블B -- INNER 생략 가능
ON 테이블A.연결컬럼 = 테이블B.연결컬럼;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;논리적 처리 순서 : FROM/JOIN (테이블 결합) &amp;rarr; WHERE (조건 필터링) &amp;rarr; SELECT (컬럼 선택) &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&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;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 한 쪽에는 데이터가 있지만, 다른 한 쪽에는 없는 데이터까지 모두 포함해서 보고싶을 때 사용하는 기술.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHiR9L/dJMcabiOrQQ/RyCJBsvsbA2sRz3FfoW5ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHiR9L/dJMcabiOrQQ/RyCJBsvsbA2sRz3FfoW5ak/img.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;499&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.6838%; margin-right: 10px;&quot; data-widthpercent=&quot;49.26&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHiR9L/dJMcabiOrQQ/RyCJBsvsbA2sRz3FfoW5ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHiR9L%2FdJMcabiOrQQ%2FRyCJBsvsbA2sRz3FfoW5ak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;530&quot; height=&quot;499&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cx0Mtl/dJMcahJ59B1/ZedmSlyaZFyRk5ftXz7ib0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cx0Mtl/dJMcahJ59B1/ZedmSlyaZFyRk5ftXz7ib0/img.png&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;499&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.1535%;&quot; data-widthpercent=&quot;50.74&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cx0Mtl/dJMcahJ59B1/ZedmSlyaZFyRk5ftXz7ib0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcx0Mtl%2FdJMcahJ59B1%2FZedmSlyaZFyRk5ftXz7ib0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;546&quot; height=&quot;499&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;LEFT (OUTER) JOIN은 왼쪽 테이블을, RIGHT (OUTER) JOIN은 오른쪽 테이블을 기준으로 삼는다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기준으로 되는 테이블의 모든 데이터를 결과에 포함시킨다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그 다음, ON 조건에 맞는 데이터를 반대 테이블에서 찾아 옆에 붙여준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;만약 반대 테이블에 짝이 맞는 데이터가 없다면, 그 자리는 NULL 값으로 채워진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;셀프 조인&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자기 참조 관계의 데이터를 한 번의 쿼리로 조회하기 위해 사용하는 기술.&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc; color: #24292f; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;자기 참조 관계란 한 테이블 안에서 자신의 컬럼이 같은 테이블의 다른 컬럼을 참조하는 구조를 말한다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;INNER JOIN &amp;rarr; NULL이면 안나온다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;LEFT JOIN &amp;rarr; NULL인 것까지 다 나온다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;크로스 조인&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;애초에 짝이나 관계가 없는 두 집단을 가지고 가능한 모든 조합을 만들어내야 할 때 사용하는 기술.&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc; color: #24292f; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;조인 조건 없이 한쪽 테이블의 모든 행을 다른 쪽 테이블의 모든 행과 하나씩 전부 연결하는 가장 단순한 조인 방식이다. 그러나, 크로스 조인은 결과의 행 수가 기하급수적으로 늘어날 수 있기 때문에 가급적 사용하지 않는 것이 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;크로스 조인의 결과는 수학 용어로 카테시안 곱 또는 데카르트 곱이라고 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DB</category>
      <category>CROSS JOIN</category>
      <category>Database</category>
      <category>inner join</category>
      <category>left join</category>
      <category>Right Join</category>
      <category>self join</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/408</guid>
      <comments>https://3uomlkh.tistory.com/408#entry408comment</comments>
      <pubDate>Thu, 27 Nov 2025 15:26:43 +0900</pubDate>
    </item>
    <item>
      <title>[CS] 디자인 패턴 종류와 간단한 개념 정리</title>
      <link>https://3uomlkh.tistory.com/407</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;color: #666666; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;해당 포스팅은 &amp;lt;면접을 위한 CS 전공지식 노트&amp;gt; 를 읽고 정리한 내용입니다.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;디자인 패턴이란 프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용해 해결할 수 있도록 하나의 '규약' 형태로 만들어 놓은 것이다. 디자인 패턴의 종류에는 싱글톤 패턴, 팩토리 패턴, 전략 패턴, 옵저버 패턴, 프록시 패턴, 이터레이터 패턴, 노출모듈 패턴, MVC 패턴, MVP 패턴, MVVM 패턴 등이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;싱글톤 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;장점 : 사용하기 쉬움, 인스턴스 생성 시 비용 절감&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;단점 : 의존성이 높아짐, TDD를 하기 어려움(각 테스트마다 독립적인 인스턴스를 만들기 어려움)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;싱글톤 패턴은 단순하고 실용적이지만 모듈 간의 결합을 강하게 만들 수 있다는 단점이 있다. 이때 &lt;b&gt;의존성 주입(DI, Dependency Injection)&lt;/b&gt;을 통해 모듈 간의 결합을 느슨하게 만들 수 있다. 의존성 주입을 원칙은 &lt;span style=&quot;text-align: start;&quot;&gt;상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 한다는 것이다. 또한 &lt;span style=&quot;text-align: start;&quot;&gt;둘 다 추상화에 의존해야 하며, 이때 추상화는 세부사항에 의존하지 않는다.&amp;nbsp;&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;  의존성 주입의 장단점&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점 : 모듈을 쉽게 교체 가능 &amp;rarr; 테스팅, 마이그레이션 쉬워짐&lt;br /&gt;단점 : 모듈들이 더욱더 분리되어 복잡성 증가&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;팩토리 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;상위 클래스와 하위 클래스가 분리됨&amp;nbsp;&amp;rarr; 느슨한 결합을 가짐&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;상위 클래스에서는 인스턴스 생성 방식에 대해 알 필요 없음 &amp;rarr; 더 많은 유연성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;객체 생성 로직이 분리되어 있음 &amp;rarr; 코드를 리팩토링 시 한 곳만 고칠 수 있어 유지 보수성 증가&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;전략 패턴(= 정책 패턴)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;객체의 행위를 바꾸고 싶을 때 직접 수정하지 않고 '캡슐화한 알고리즘'을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;전략 패턴은 알고리즘이나 동작 방식을 유연하게 교체할 수 있도록 하는 패턴이다. 예를 들어, 결제 시스템에서 PaymentStrategy를 바꿔서 CardPayment, KakaoPay 등으로 교체할 수 있다. 즉, 동작을 유연하게 바꾸는 것이 목적이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;옵저버 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다&amp;nbsp; 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 패턴.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;옵저버 패턴은 주로 이벤트 기반 시스템에 사용되며, MVC 패턴에도 자주 사용된다. 옵저버 패턴에서의 주체, 옵저버의 개념은 다음과 같다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;주체 : 객체의 상태 변화를 보고 있는 관찰자로, 상태 변경 시 옵저버에게 알림을 보냄&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;옵저버 : 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 '추가 변경 사항'이 생기는 객체들을 의미함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;옵저버 패턴을 구현하는 방법에는 프록시 객체를 사용하는 방법이 있다. 프록시 객체를 통해 객체의 속성이나 메서드 변화 등을 감지하고 이를 미리 설정해놓은 옵저버들에게 전달하는 방법으로 구현한다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 프록시 객체&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;어떠한 대상의 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 작업을 가로챌 수 있는 객체를 의미한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;프록시 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 패턴.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;프록시 패턴은 실제 객체에 대한 접근을 제어하기 위해 그 객체를 대신하는 대리 객체를 두는 패턴이다. 이를 통해&amp;nbsp; 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;프록시 서버를 통해 포트 번호를 바꿔서 사용자가 실제 서버의 포트에 접근하지 못하게 하거나, 공격자의 DDOS 공격을 차단할 수 있다. 또한 CDN을 프록시 서버로 달아서 캐싱 처리를 용이하게 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;nginx로 Node.js로 이루어진 서버의 앞단에 둬서 버퍼 오버플로우를 해결하거나 CloudFlare를 둬서 캐싱, 로그 분석 등을 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;  DN(Content Delivery Network)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;각 사용자가 인터넷과 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크를 말한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;이터레이터 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;이터레이터를 사용하여 컬렉션의 요소들에 접근하는 패턴.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다음 예제를 보면 서로 다른 자료 구조인 set과 map이지만, 똑같은 &lt;b&gt;for a of b&lt;/b&gt; 라는 이터레이터 프로토콜을 통해 순회하는 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761141846525&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const mp = new Map()
mp.set('a', 1)
mp.set('b', 2)
mp.set('c', 3)
const st = new Set()
st.add(1)
st.add(2)
st.add(3)
for (let a of mp) console.log(a)
for (let a of st) console.log(a)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;노출 모듈 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;즉시 실행 함수를 통해 private, public 같은 접근 제어자를 만드는 패턴.&amp;nbsp;&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자바 스크립트는 접근 제어자가 존재하지 않고 전역 범위에서 스크립트가 실행됨 -&amp;gt; 노출 모듈 패턴을 통해 접근 제어자를 구현하기도 함.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;MVC 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;모델(Model), 뷰(View), 컨트롤러(Controller)로 이루어진 패턴.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MVC 패턴을 이용하면 애플리케이션의 구성 요소를 세 가지 역할로 구분하여 개발 프로세스에서 가각의 구성 요소에만 집중하여 개발할 수 있다. MVC 패턴을 이용한 대표적인 라이브러리에는 리액트(React.js)가 있다..라고 책에 나와있는데, 검색해보니 실제로 React 자체는 MVC를 완전히 구현한 라이브러리는 아니고, View 역할만 담당하는 컴포넌트 기반 라이브러리라고 한다. Controller나 Model 역할은 외부 상태 관리 라이브러리(Redux, Recoil 등)나 개발자가 직접 구현해야 한다. 즉, React로 MVC 구조를 구현할 수는 있지만, React자체가 MVC는 아니다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MVC 패턴의 장단점은 다음과 같다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점 : 재사용성과 확장성이 용이함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단점 : 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해짐&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: left;&quot;&gt; 모델(Model), 뷰(View), 컨트롤러(Controller)의 개념은 다음과 같다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 모델(Model)&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;애플리케이션의 데이터(데이터베이스, 상수, 변수 등)를 의미&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;뷰(View)&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용자 인터페이스 요소(inputbox, checkbox, textarea 등)를 의미&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;컨트롤러(Controller)&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하나 이상의 모델과 뷰를 잇는 다리 역할을 하며, 메인 로직을 담당함.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;모델과 뷰의 생명주기 관리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;모델이나 뷰의 변경 통지를 받으면 이를 해석해 각각의 구성 요소에 해당 내용에 대해 알려줌&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;MVP 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;MVC 패턴으로부터 파생된 패턴으로, C에 해당하는 컨트롤러(Controller)가 프레젠터(Presenter) 로 교체된 패턴.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MVP패턴에서 뷰와 프레젠터는 일대일 관계이기 때문에 MVC 패턴보다 더 강한 결합을 가진다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;MVVM 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;MVC의 C에 해당하는 컨트롤러(Controller)가 뷰모델(View Model)로 바뀐 패턴.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;MVVM 패턴은 &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;모델(Model), 뷰(View),&amp;nbsp;&lt;/span&gt; 뷰모델(View Model)로 이루어진 패턴이다. MVVM 패턴을 이용한 대표적인 라이브러리에는 뷰(Vue.js)가 있다. 이 패턴에서 &lt;/span&gt;뷰모델이란 뷰를 더 추상화한 계층으로, MVC 패턴과는 다르게 커맨드와 데이터 바인딩을 가지는 것이 특징이다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점 : 뷰와 뷰모델 사이의 양방향 데이터 바인딩 지원, 별도의 코드 수정 없이 UI 재사용 가능, 단위 테스트가 쉬움&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단점 : 양방향 바인딩이 많아질수록 복잡해짐&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; MVC 패턴과 MVVM 패턴의 차이&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MVVM 패턴은 MVC 패턴과 다르게 커맨드와 데이터 바인딩을 가지는 것이 특징이다. 뷰와 뷰모델 사이의 양방향 데이터 바인딩을 지원하며 UI를 별도의 코드 수정 없이 재사용할 수 있고 단위 테스팅하기 쉽다는 장점이 있다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>TIL</category>
      <category>디자인패턴</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/407</guid>
      <comments>https://3uomlkh.tistory.com/407#entry407comment</comments>
      <pubDate>Wed, 22 Oct 2025 23:27:40 +0900</pubDate>
    </item>
    <item>
      <title>'향로' 와 함께하는 추석 완강 챌린지 회고  </title>
      <link>https://3uomlkh.tistory.com/406</link>
      <description>&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;1118&quot; data-origin-height=&quot;711&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dr4TR7/btsQ8nb011h/kwDzexqjYUkS9dBygrhoQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dr4TR7/btsQ8nb011h/kwDzexqjYUkS9dBygrhoQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dr4TR7/btsQ8nb011h/kwDzexqjYUkS9dBygrhoQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdr4TR7%2FbtsQ8nb011h%2FkwDzexqjYUkS9dBygrhoQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1118&quot; height=&quot;711&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;711&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;추석 연휴 동안 인프런에서 하는 챌린지에 참여했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;연휴가 길어 늘어질 것을 대비해 신청한 챌린지였는데, 늘어지는 것을 아예 막을 순 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;내가 수강한 강의는 &lt;b&gt;&amp;lt;외워서 끝내는 네트워크 핵심이론 - 기초&amp;gt;&lt;/b&gt; 강의다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;한 번 더 복습해야할 것 같지만, 학부 때 배운 (다 까먹어가는) 네트워크 지식을 핵심만 골라 뇌에 새길 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;769&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t7Jbz/btsQ74jIhVA/TKuczGEod9belJe9SK20pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t7Jbz/btsQ74jIhVA/TKuczGEod9belJe9SK20pK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t7Jbz/btsQ74jIhVA/TKuczGEod9belJe9SK20pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft7Jbz%2FbtsQ74jIhVA%2FTKuczGEod9belJe9SK20pK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;579&quot; height=&quot;634&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;769&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다시 한 번 복습하고 응용편도 들어봐야겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인프런 복습 기능 출시 기원 정권지르기 1일차.. &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;1060&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eaZ4nV/btsQ77f5vyT/jgxvaZkkvzx7Yqh2MrfRT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eaZ4nV/btsQ77f5vyT/jgxvaZkkvzx7Yqh2MrfRT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eaZ4nV/btsQ77f5vyT/jgxvaZkkvzx7Yqh2MrfRT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeaZ4nV%2FbtsQ77f5vyT%2FjgxvaZkkvzx7Yqh2MrfRT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1060&quot; height=&quot;675&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;675&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;향로님의 라이브를 들으며 &lt;u&gt;&lt;i&gt;&lt;b&gt;아궁이 속 불씨를 꺼지지 않도록 하는 것이 중요하다는 것&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;을 깨달았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그런데 내가 잘못 알고 있었던 건, 그 불씨가 꼭 개발에 국한되어야 한다는 것은 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;개발이 될 수도 있고, 다른 새로운 공부, 혹은 취미 활동이 될 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;중요한 건 지치지 않고 불씨가 꺼지지 않도록 하는 것!&lt;/span&gt;&lt;/p&gt;</description>
      <category>회고</category>
      <category>인프런</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/406</guid>
      <comments>https://3uomlkh.tistory.com/406#entry406comment</comments>
      <pubDate>Tue, 14 Oct 2025 11:40:26 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] Docker Compose로 여러 컨테이너 띄우기</title>
      <link>https://3uomlkh.tistory.com/405</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt; 인프런 &amp;lt;비전공자도 이해할 수 있는 Docker 입문/실전&amp;gt; 강의를 토대로 정리한 내용입니다. &lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; &amp;nbsp;Docker Compose란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이전에 배운 Docker CLI, Dockerfile 등에 대해 제대로 숙지했다면 Docker Compose는 어렵지 않다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Docker Compose는 쉽게 말하면 여러 컨테이너를 한 번에 띄우고 싶을 때 사용한다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Docker Compose&lt;/b&gt; : 여러 개의 Docker 컨테이너들을 하나의 서비스로 정의하고 구성해 하나의 묶음으로 관리할 수 있게 도와주는 툴.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Docker Compose를 사용하는 이유?&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여러 개의 컨테이너를 관리하는 데 용이하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;복잡한 명령어로 실행하던 것을 간소화 시킬 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;자주 사용하는 Docker Compose CLI 명령어&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; compose.yml에서 정의한 컨테이너 실행&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker compose up : 포그라운드에서 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker compose up -d : 백그라운드에서 실행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&amp;nbsp;&lt;b&gt;Docker Compose로 실행시킨 컨테이너 확인&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker compose ps : 실행 중인 컨테이너 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker compose ps -a : 정의된 모든 컨테이너 확인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Docker Compose 로그 확인&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker compose logs&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker compose up --build : 포그라운드에서 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker compose up --build -d : 백그라운드에서 실행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; 이미지 다운받기 / 업데이트하기&lt;/b&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker compose pull&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;로컬 환경에 이미지가 없다면 다운로드, 있는데 DockerHub의 이미지와 다를 경우 업데이트.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Docker Compose에서 이용한 컨테이너 종료&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker compose down&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Docker Compose로 MySQL, Redis 띄우기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Docker Compose로 MySQL과 Redis를 동시에 띄워보는 실습을 진행했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;우선 compose.yml 파일은 다음과 같이 작성했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1759461624942&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  my-db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: pwd1234
    volumes:
      - ./mysql_data:/var/lib/mysql
    ports:
      - 3306:3306
  my-cache-server:
    image: redis
    ports:
      - 6379:6379&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Docker Compose에서 하나의 컨테이너를 서비스(service) 라고 한다. 따라서 여러 개의 서비스를 적기 전에 &lt;b&gt;services&lt;/b&gt; 라는 명령어를 적는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;services 밑에는 컨테이너, 즉 서비스에 이름을 붙여 적는다. &lt;/span&gt;my-db, my-cache-server와 같이 내가 적고 싶은 이름을 적는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;image에는 어떤 이미지를 기반으로 컨테이너를 띄울 것인지 적는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이제 docker compose up -d 명령어로 compose.yml에서 정의한 컨테이너를 실행시킨다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그리고나서 docker ps (혹은 docker compose ps)로 확인해보니 MySQL과 Redis가 동시에 잘 띄워진 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 Docker Compose를 사용하면 각각의 컨테이너를 따로 띄울 필요 없이 한 번에 띄울 수 있다.&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;1621&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8Mry3/dJMb9MbNuCS/Y6SD7h1VTOXk9yba6B9dOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8Mry3/dJMb9MbNuCS/Y6SD7h1VTOXk9yba6B9dOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8Mry3/dJMb9MbNuCS/Y6SD7h1VTOXk9yba6B9dOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8Mry3%2FdJMb9MbNuCS%2FY6SD7h1VTOXk9yba6B9dOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1621&quot; height=&quot;273&quot; data-origin-width=&quot;1621&quot; data-origin-height=&quot;273&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker compose down 명령어로 컨테이너들을 한 번에 내릴 수 있다.&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;1592&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GFBx1/dJMb9OAGz3d/UaGjQX2kq0av9IeBa13WkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GFBx1/dJMb9OAGz3d/UaGjQX2kq0av9IeBa13WkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GFBx1/dJMb9OAGz3d/UaGjQX2kq0av9IeBa13WkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGFBx1%2FdJMb9OAGz3d%2FUaGjQX2kq0av9IeBa13WkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1592&quot; height=&quot;204&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Docker</category>
      <category>docker</category>
      <category>docker compose</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/405</guid>
      <comments>https://3uomlkh.tistory.com/405#entry405comment</comments>
      <pubDate>Wed, 8 Oct 2025 23:59:38 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring Boot에서 URI가 컨트롤러에 매핑되는 흐름</title>
      <link>https://3uomlkh.tistory.com/404</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;URI란?&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;URI(Uniform Resource Identifier)&lt;/b&gt; : 인터넷 상의 자원을 식별하는 통합 자원 식별자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;자원의 위치뿐만 아니라 이름이나 식별 정보를 포함하여 자원을 고유하게 지칭하는 문자열이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; URI는 URL, URN의 상위 개념이다.&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;567&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/owQDB/btsQI5WwSYB/43g42N6H56j7vyjN4LILIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/owQDB/btsQI5WwSYB/43g42N6H56j7vyjN4LILIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/owQDB/btsQI5WwSYB/43g42N6H56j7vyjN4LILIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FowQDB%2FbtsQI5WwSYB%2F43g42N6H56j7vyjN4LILIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;621&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;URI와 URL의 차이&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;URL(Uniform Resource Locator)&lt;/b&gt; : URI의 한 형태로, 네트워크상에서 자원의 위치와 접근 방법(프로토콜, 도메인, 경로 등)을 명시하는 규약.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;모든 URL은 URI이지만, 모든 URI가 URL은 아니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Spring Boot에서 URI가 컨트롤러에 매핑되는 흐름&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이제 Spring Boot에서 URI가 컨트롤러에 매핑되는 흐름을 살펴보자. DispatcherServlet의 동작 방식이라고 봐도 무방하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;참고 자료로 본 블로그에 있던 DispatcherServlet의 처리 과정 그림을 차근 차근 이해하기 위해 직접 그려봤다.&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;Spring_Boot_URI_매핑.png&quot; data-origin-width=&quot;2040&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8yFFo/btsQDcJpLlA/hK0shUKMiosEFEpTNuvteK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8yFFo/btsQDcJpLlA/hK0shUKMiosEFEpTNuvteK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8yFFo/btsQDcJpLlA/hK0shUKMiosEFEpTNuvteK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8yFFo%2FbtsQDcJpLlA%2FhK0shUKMiosEFEpTNuvteK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2040&quot; height=&quot;720&quot; data-filename=&quot;Spring_Boot_URI_매핑.png&quot; data-origin-width=&quot;2040&quot; data-origin-height=&quot;720&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1. 사용자(클라이언트)로부터 요청이 들어오면, 가장 먼저 DispatcherServlet이 받는다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; DispatcherServlet은 &lt;b&gt;HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임&lt;/b&gt;하는 &lt;u&gt;&lt;b&gt;프론트 컨트롤러&lt;/b&gt;&lt;/u&gt;이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;2. &lt;b&gt;HandlerMapping&lt;/b&gt;이 요청 URI와 매핑된 컨트롤러를 찾아준다. &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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; HandlerMapping은 &lt;b&gt;@RequestMapping, @GetMapping&lt;/b&gt; 같은 어노테이션 정보를 기준으로 &lt;b&gt;어떤 컨트롤러가 처리할 수 있는지 결정&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;3. &lt;b&gt;HandlerAdapter&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; HandlerAdapter는 어댑터 패턴 개념을 차용하여, DispatcherServlet이 &lt;b&gt;컨트롤러의 구현 방식에 구애받지 않고 요청을 위임&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;4. HandlerAdapter가 컨트롤러를 호출한다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 선택된 HandlerAdapter가 실제 RestController 메서드를 실행한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 실행 전에 &lt;b&gt;@PathVariable, @RequestBody&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. &lt;/b&gt;&lt;b&gt;컨트롤러가 응답 데이터를 반환한다. (ResponseEntity, DTO 등)&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;비즈니스 로직을 수행하기 위해 Service 계층을 호출하고, 필요 시 Repository를 통해 DB에 접근한다.&lt;/span&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 이 반환값을 HandlerAdapter가 받는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;6. HandlerAdapter가 ResponseEntity를 처리한다.&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; @RestController&lt;/b&gt;가 붙은 컨트롤러 메서드의 반환값은 뷰가 아닌 응답 본문으로 처리되며, JSON/XML 같은 형식으로 변환할 때는&lt;b&gt; HttpMessageConverter가 직렬화를 수행&lt;/b&gt;한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; 뷰 템플릿이라면 ViewResolver가 화면을 렌더링&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;7. 최종적으로 가공된 응답 객체를 DispatcherServlet에 다시 전달한다.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;8. DispatcherServlet은 응답을 HTTP 응답으로 변환하여 클라이언트에 반환한다.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[ 참고 자료 ]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1758287378880&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;  URL / URI / URN 차이점 - 한방 이해하기&quot; data-og-description=&quot;URL / URI / URN 차이점 우리가 브라우저를 통해 웹을 이용하게 된다면, URL 단어는 익숙할 것이다. 하지만 가끔 뭔가 비스무리하면서도 다른 URI와 URN 이라는 단어를 사용하기도 하는데 이번 시간에&quot; data-og-host=&quot;inpa.tistory.com&quot; data-og-source-url=&quot;https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-URL-URI-%EC%B0%A8%EC%9D%B4#uri_uniform_resource_identifier&quot; data-og-url=&quot;https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-URL-URI-%EC%B0%A8%EC%9D%B4&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fy3j2/hyZJypa4zc/vMs3GvU0hDHzYJ3JSJcXR0/img.jpg?width=800&amp;amp;height=480&amp;amp;face=0_0_800_480,https://scrap.kakaocdn.net/dn/Tum6S/hyZJizTZkc/DrunSXYbAcg9F3p2NbkkwK/img.jpg?width=800&amp;amp;height=480&amp;amp;face=0_0_800_480,https://scrap.kakaocdn.net/dn/oWLQQ/hyZJnBbtVD/XdPYRmtV4n1Rnu79ekK0z0/img.png?width=637&amp;amp;height=569&amp;amp;face=0_0_637_569&quot;&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-URL-URI-%EC%B0%A8%EC%9D%B4#uri_uniform_resource_identifier&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-URL-URI-%EC%B0%A8%EC%9D%B4#uri_uniform_resource_identifier&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fy3j2/hyZJypa4zc/vMs3GvU0hDHzYJ3JSJcXR0/img.jpg?width=800&amp;amp;height=480&amp;amp;face=0_0_800_480,https://scrap.kakaocdn.net/dn/Tum6S/hyZJizTZkc/DrunSXYbAcg9F3p2NbkkwK/img.jpg?width=800&amp;amp;height=480&amp;amp;face=0_0_800_480,https://scrap.kakaocdn.net/dn/oWLQQ/hyZJnBbtVD/XdPYRmtV4n1Rnu79ekK0z0/img.png?width=637&amp;amp;height=569&amp;amp;face=0_0_637_569');&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;  URL / URI / URN 차이점 - 한방 이해하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;URL / URI / URN 차이점 우리가 브라우저를 통해 웹을 이용하게 된다면, URL 단어는 익숙할 것이다. 하지만 가끔 뭔가 비스무리하면서도 다른 URI와 URN 이라는 단어를 사용하기도 하는데 이번 시간에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;inpa.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1758287367435&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring 웹 요청 흐름(DispatcherServlet)&quot; data-og-description=&quot;회사에서 신입 개발자 교육용으로 만들었던 자료입니다. 스프링에서 웹 요청 처리 흐름에 대한 내용으로 어...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://m.blog.naver.com/shyj6611/221583360087&quot; data-og-url=&quot;https://blog.naver.com/shyj6611/221583360087&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dh0NwO/hyZJvMKc2w/iIuIwvmRyNWpfBYGqIHmz1/img.png?width=743&amp;amp;height=418&amp;amp;face=0_0_743_418&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/shyj6611/221583360087&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://m.blog.naver.com/shyj6611/221583360087&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dh0NwO/hyZJvMKc2w/iIuIwvmRyNWpfBYGqIHmz1/img.png?width=743&amp;amp;height=418&amp;amp;face=0_0_743_418');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring 웹 요청 흐름(DispatcherServlet)&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;figure id=&quot;og_1758287393996&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring] Dispatcher-Servlet(디스패처 서블릿)이란? 디스패처 서블릿의 개념과 동작 과정&quot; data-og-description=&quot;이번에는 servlet의 심화 또는 대표주자인 dispatcher-servlet에 대해서 알아보도록 하겠습니다. 1. Dispatcher-Servlet(디스패처 서블릿)의 개념 [ Dispatcher-Servlet(디스패처 서블릿) 이란? ] 디스패처 서블릿의 &quot; data-og-host=&quot;mangkyu.tistory.com&quot; data-og-source-url=&quot;https://mangkyu.tistory.com/18&quot; data-og-url=&quot;https://mangkyu.tistory.com/18&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ENRz3/hyZJzHtZkF/UMl1TriPtANEi30FUBH1n1/img.png?width=800&amp;amp;height=385&amp;amp;face=0_0_800_385,https://scrap.kakaocdn.net/dn/cPPDke/hyZJtOV6ZS/AlU8sLm6D3Rv7bRG060uv1/img.png?width=800&amp;amp;height=385&amp;amp;face=0_0_800_385,https://scrap.kakaocdn.net/dn/cQaaMh/hyZJOSatwo/Jha1ztPVX45XMLzAXjYy0K/img.png?width=2306&amp;amp;height=872&amp;amp;face=0_0_2306_872&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mangkyu.tistory.com/18&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ENRz3/hyZJzHtZkF/UMl1TriPtANEi30FUBH1n1/img.png?width=800&amp;amp;height=385&amp;amp;face=0_0_800_385,https://scrap.kakaocdn.net/dn/cPPDke/hyZJtOV6ZS/AlU8sLm6D3Rv7bRG060uv1/img.png?width=800&amp;amp;height=385&amp;amp;face=0_0_800_385,https://scrap.kakaocdn.net/dn/cQaaMh/hyZJOSatwo/Jha1ztPVX45XMLzAXjYy0K/img.png?width=2306&amp;amp;height=872&amp;amp;face=0_0_2306_872');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring] Dispatcher-Servlet(디스패처 서블릿)이란? 디스패처 서블릿의 개념과 동작 과정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 servlet의 심화 또는 대표주자인 dispatcher-servlet에 대해서 알아보도록 하겠습니다. 1. Dispatcher-Servlet(디스패처 서블릿)의 개념 [ Dispatcher-Servlet(디스패처 서블릿) 이란? ] 디스패처 서블릿의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mangkyu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/404</guid>
      <comments>https://3uomlkh.tistory.com/404#entry404comment</comments>
      <pubDate>Fri, 19 Sep 2025 22:25:09 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 멀티 스레드 - 스레드와 스레드 풀 이해하기</title>
      <link>https://3uomlkh.tistory.com/403</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;인프런 &amp;lt;스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술&amp;gt; 강의를 보고 정리한 내용입니다.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;멀티 스레드?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;294&quot; data-start=&quot;138&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;183&quot; data-start=&quot;138&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;스레드(Thread) &lt;/b&gt;: 애플리케이션 코드가 순차적으로 실행되는 흐름&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;221&quot; data-start=&quot;184&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;자바 애플리케이션 실행 시, main 스레드가 처음 시작&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;255&quot; data-start=&quot;222&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;한 스레드는 한 번에 &lt;b&gt;하나의 코드 라인&lt;/b&gt;만 수행&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;294&quot; data-start=&quot;256&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여러 작업을 동시에 처리해야 할 경우, 추가 스레드 생성 필요&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;294&quot; data-start=&quot;256&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;예: 웹 서버에서 여러 요청을 동시에 처리하려면 멀티 스레드가 필수&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;웹 서버가 요청마다 새 스레드를 만들면 어떨까?&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;403&quot; data-start=&quot;395&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;480&quot; data-start=&quot;404&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;419&quot; data-start=&quot;404&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;동시 요청 처리 가능&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;449&quot; data-start=&quot;420&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;하나의 요청이 지연되더라도 나머지는 정상 동작&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;480&quot; data-start=&quot;450&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;서버 리소스 허용 범위 내에서 처리량 확장 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;490&quot; data-start=&quot;482&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;642&quot; data-start=&quot;491&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;523&quot; data-start=&quot;491&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스레드 생성 비용 높음 &amp;rarr; 요청 폭주 시 응답 지연&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;605&quot; data-start=&quot;524&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;컨텍스트 스위칭(Context Switching)&lt;/b&gt; 비용 발생&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;605&quot; data-start=&quot;568&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;OS가 CPU를 한 스레드에서 다른 스레드로 전환할 때 발생&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;642&quot; data-start=&quot;606&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스레드 생성이 무제한 &amp;rarr; 리소스 한계 초과 시 서버 다운 위험&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;스레드 풀(Thread Pool)로 해결하기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;요청마다 스레드를 만들 때 생기는 문제를 해결하기 위해 스레드 풀을 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스레드 풀은 필요한 스레드를 보관하고 관리하는 곳이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스레드 풀에 생성 가능한 스레드의 최대치를 관리한다. 예를 들어 톰캣은 최대 200개가 기본 설정이며, 변경 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;동작 방식&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;필요한 스레드를 미리 생성해 스레드 풀에 보관한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;요청이 들어오면 풀에서 스레드를 꺼내 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;사용 완료 후 다시 풀에 반납한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;815&quot; data-start=&quot;785&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스레드 생성/소멸 비용(CPU) 절감 &amp;rarr; 응답 속도 향상&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;848&quot; data-start=&quot;816&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;최대 스레드 수 제한으로 요청 폭주에도 안정적 처리 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스레드 풀이 너무 클 경우 &lt;span style=&quot;text-align: left;&quot;&gt;&amp;rarr; 메모리 낭비&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;실무 팁&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;958&quot; data-start=&quot;861&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;WAS 튜닝 핵심 : &lt;b&gt;최대 스레드(max thread) 수&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;958&quot; data-start=&quot;893&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;922&quot; data-start=&quot;893&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;너무 낮으면 &amp;rarr; 동시 요청 많을 때 지연 발생&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;958&quot; data-start=&quot;925&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;너무 높으면 &amp;rarr; CPU/메모리 한계 초과, 서버 다운&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1024&quot; data-start=&quot;959&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;장애 대응&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1024&quot; data-start=&quot;971&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;995&quot; data-start=&quot;971&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;클라우드 환경 : 서버 수 늘리고 튜닝&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1024&quot; data-start=&quot;998&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;온프레미스 : 최대 스레드 수 최적화 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;적정 스레드 수&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1077&quot; data-start=&quot;1041&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;애플리케이션 로직, CPU/메모리, IO 리소스 상황에 따라 다름 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: left;&quot;&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 실제 서비스와 유사한 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;성능 테스트 필수&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1135&quot; data-start=&quot;1096&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;성능 테스트 도구 : Apache AB, JMeter, nGrinder&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;WAS와 멀티 스레드&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;멀티 스레드는 복잡하고 신경써야 하는 부분이 많은데, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;WAS가 있기 때문에 &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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1248&quot; data-start=&quot;1204&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;WAS가 멀티 스레드 처리를 담당 &amp;rarr; 개발자는 싱글 스레드처럼 코딩 가능&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1324&quot; data-start=&quot;1249&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;단, 싱글톤 객체(서블릿, 스프링 빈) 사용 시 주의&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1324&quot; data-start=&quot;1277&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1324&quot; data-start=&quot;1277&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;공유 변수/멤버 변수는 동시 접근 문제 발생 가능 &amp;rarr; 스레드 안전 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;추가 개념&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 data-end=&quot;1419&quot; data-start=&quot;1361&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스레드 안전(Thread Safety) : 여러 스레드가 동시에 접근해도 데이터 일관성 유지&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1479&quot; data-start=&quot;1420&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;동시성 문제(Concurrency Issue) : race condition, deadlock&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1543&quot; data-start=&quot;1480&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;자바 스레드 풀 활용 예시 : ExecutorService, ThreadPoolExecutor&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>TIL</category>
      <category>Thread</category>
      <category>WAS</category>
      <category>멀티스레드</category>
      <category>스레드풀</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/403</guid>
      <comments>https://3uomlkh.tistory.com/403#entry403comment</comments>
      <pubDate>Thu, 11 Sep 2025 18:24:45 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] Dockerfile 개념 및 사용법</title>
      <link>https://3uomlkh.tistory.com/402</link>
      <description>&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;인프런 &amp;lt;비전공자도 이해할 수 있는 Docker 입문/실전&amp;gt; 강의를 토대로 정리한 내용입니다.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  도커파일(Dockerfile&lt;/b&gt;&lt;b&gt;)&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;도커파일(&lt;b&gt;Dockerfile&lt;/b&gt;)&lt;/b&gt; : Docker 이미지를 만들기 위한 파일. 텍스트 파일로 작성되며, 확장자는 Dockerfile이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Dockerfile은 다음과 같이 만들 수 있다. 아래 Dockerfile은 강의 실습용으로 간단하게 작성한 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tGQpH/btsQkgq7lPt/QKBKd3hSI6R2gb8PnucC5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tGQpH/btsQkgq7lPt/QKBKd3hSI6R2gb8PnucC5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tGQpH/btsQkgq7lPt/QKBKd3hSI6R2gb8PnucC5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtGQpH%2FbtsQkgq7lPt%2FQKBKd3hSI6R2gb8PnucC5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;547&quot; height=&quot;545&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Dockerfile &lt;/b&gt;&lt;b&gt;명령어&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Dockerfile 명령어 요약&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;▪️FROM : 베이스 이미지 생성, 모든 Dockerfile은 FROM으로 시작하며, 태그명을 적지 않을시 최신 버전 사용&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;▪️COPY : 호스트 파일을 복사해 컨테이너로 전달&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;▪️RUN : 이미지 생성 과정 중 컨테이너 내에서 명령어 실행&lt;/span&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;▪️ENTRYPOINT : 컨테이너 시작 시 항상 실행&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;▪️WORKDIR : 작업 디렉터리 설정&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;▪️EXPOSE : 컨테이너 포트 공개&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Spring Boot 프로젝트 실행 - FROM, &lt;/b&gt;&lt;b&gt;COPY, &lt;/b&gt;&lt;b&gt;ENTRYPOINT&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;FROM, COPY, ENTRYPOINT 명령어를 사용해 Spring Boot 프로젝트를 Docker로 실행시켰다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1756974931287&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM openjdk:17-jdk

COPY build/libs/*SNAPSHOT.jar app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위 Dockerfile에서는 JDK 17 베이스 이미지로 컨테이너를 띄웠다. Spring Boot 프로젝트는 &lt;b&gt;.jar&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;410&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKyWly/btsQk96Zvko/XRrEtIIEFgj7Rt9Rkr5Kw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKyWly/btsQk96Zvko/XRrEtIIEFgj7Rt9Rkr5Kw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKyWly/btsQk96Zvko/XRrEtIIEFgj7Rt9Rkr5Kw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKyWly%2FbtsQk96Zvko%2FXRrEtIIEFgj7Rt9Rkr5Kw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;410&quot; height=&quot;318&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;318&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;COPY build/libs/*SNAPSHOT.jar app.jar&lt;/span&gt;는 build/libs/ 안 SNAPSHOT.jar로 끝나는 파일을 컨테이너 안 app.jar로 복사하겠다는 의미다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;E&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;NTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]&lt;/span&gt;는 컨테이너가 실행될 때 최초로 실행시킬 명령어를 작성한 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;b&gt;.dockerignore 파일을 통해 특정 파일 또는 폴더를 COPY 하지 못하도록 할 수 있다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Next.js 프로젝트 실행 - RUN, &lt;/b&gt;&lt;b&gt;WORKDIR, &lt;/b&gt;&lt;b&gt;EXPOSE&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다음과 같이 Dockerfile을 작성해 Next.js 프로젝트를 &lt;span style=&quot;text-align: start;&quot;&gt;Docker로 실행시켰다.&lt;/span&gt; 이번에는&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;RUN, WORKDIR, EXPOSE 명령어도 함께 사용했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757036554609&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM node

WORKDIR /app

COPY . .

RUN npm install

RUN npm run build

EXPOSE 3000

ENTRYPOINT [ &quot;node&quot;, &quot;dist/main.js&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위 Dockerfile에서는 node 베이스 이미지로 컨테이너를 띄웠고, app 이라는 폴더 안에 파일들이 복사되도록 작성했다. 또한 호스트에 있는 모든 파일을 복사해 컨테이너로 가져간다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;RUN npm install&lt;/span&gt;와 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;RUN npm run build&lt;/span&gt;는 컨테이너에서 Next.js를 실행시키고 패키지, 의존성, 라이브러리들을 설치하는 명령어를 실행한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;EXPOSE 3000&lt;/span&gt;은 있으나 없으나 작동하는 방식에는 영향을 미치진 않고, 3000번 포트에서 실행될 것이라고 문서화하는 역할을 한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Docker</category>
      <category>docker</category>
      <category>Dockerfile</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/402</guid>
      <comments>https://3uomlkh.tistory.com/402#entry402comment</comments>
      <pubDate>Fri, 5 Sep 2025 11:26:19 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 날짜 범위 필터링하기</title>
      <link>https://3uomlkh.tistory.com/401</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; SQL로 데이터를 조회하다 보면 특정 기간의 데이터를 가져와야 할 때가 많다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;예를 들어, &lt;b&gt;&quot;2022년 10월에 작성된 글만 조회&quot;&lt;/b&gt; 같은 조건이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;사실 나는 SQL 문제를 풀 때 LIKE 문법이 익숙해서 자주 사용하는데, 날짜 범위 필터링을 하는 방법은 더 많다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 날짜 범위 필터링 시 자주 쓰이는 SQL 문법들을 정리해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;LIKE&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1756341341377&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE CREATED_DATE LIKE '2022-10%'&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;LIKE는 &lt;b&gt;문자열 비교 연산자&lt;/b&gt;로, '2022-10%' 라는 패턴을 주면, '2022-10'으로 시작하는 문자열을 찾아준다.&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점 : 간단하고 직관적인 문법&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단점 : DATE 타입보다는 VARCHAR 처럼 문자열로 저장된 날짜에 적&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;BETWEEN&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1756341506082&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE CREATED_DATE BETWEEN '2022-10-01' AND '2022-10-31'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;BETWEEN A AND B는 &lt;b&gt;A 이상 B 이하 범위&lt;/b&gt;를 의미한다. 즉, '2022-10-01' &amp;lt;= CREATED_DATE &amp;lt;= '2022-10-31' 조건과 동일하다. 단, 컬럼이 DATETIME 타입이라면 마지막 날짜를 '2022-10-31 23:59:59'까지 지정해야 안전하다. 이 문제를 해결하기 위해서는 아래 DATE() 함수를 참고하자.&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점 : 뛰어난 가독성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단점 : CREATED_DATE가 DATETIME일 경우 문제 발생 ('2022-10-31'은 2022-10-31 00:00:00까지만 포함)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;DATE()&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1756341694465&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE DATE(CREATED_DATE) BETWEEN '2022-10-01' AND '2022-10-31'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; DATE() 함수는 DATETIME에서 &lt;b&gt;날짜 부분(YYYY-MM-DD)만 잘라내는 함수&lt;/b&gt;로, 위에서 언급한 문제점을 해결할 수 있다. 그러나 날짜만 추출해서 비교해야 하기 때문에 인덱스에 있는 값 그대로는 비교할 수 없다. 따라서 DB는 모든 행을 하나씩 읽어서 DATE() 함수를 계산해야 조건을 판별할 수 있다. 이 과정 때문에 &lt;b&gt;인덱스를 그대로 활용할 수 없고&lt;/b&gt;, 결국 &lt;b&gt;Full Table Scan&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;장점 : DATETIME 컬럼에서도 정확히 원하는 날짜만 비교 가능 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;단점 : 컬럼에 함수를 적용(DATE(CREATED_DATE))하면 DB가 인덱스를 활용하지 못함. 즉, 데이터가 많아질수록 성능이 급격히 느려질 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;YEAR() + MONTH()&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1756341919713&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE YEAR(CREATED_DATE) = 2022 AND MONTH(CREATED_DATE) = 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2039&quot; data-start=&quot;2011&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;YEAR() 함수: 날짜에서 연도를 추출&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2068&quot; data-start=&quot;2040&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;MONTH() 함수: 날짜에서 월을 추출&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;각 함수로 날짜에서 연도, 월을 추출해 2022년 10월 데이터만 걸러낼 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그러나 역시 DATE()를 사용할 때와 같은 단점이 있다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;장점 : 읽고 이해하기 쉬움&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;단점 : DATE()와 마찬가지로 인덱스를 사용하지 못하고, 성능이 떨어질 수 있음&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 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&amp;gt;= AND &amp;lt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1756342917681&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE CREATED_DATE &amp;gt;= '2022-10-01'
  AND CREATED_DATE &amp;lt; '2022-11-01'&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;시작일 이상(&amp;gt;= '2022-10-01') 다음 달 1일 미만(&amp;lt; '2022-11-01') 의 데이터를 구하는 쿼리다.10월 1일부터 10월 31일까지 모든 데이터가 포함된다. &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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;장점 : DATE / DATETIME 모두 안정적으로 처리 가능, 인덱스 활용 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;</description>
      <category>MySQL</category>
      <category>MySQL</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/401</guid>
      <comments>https://3uomlkh.tistory.com/401#entry401comment</comments>
      <pubDate>Thu, 28 Aug 2025 10:16:14 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] Docker Volume 개념 및 사용법</title>
      <link>https://3uomlkh.tistory.com/400</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt; 인프런 &amp;lt;비전공자도 이해할 수 있는 Docker 입문/실전&amp;gt; 강의를 토대로 정리한 내용입니다. &lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  도커볼륨(&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Docker Volume&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;)&lt;/b&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;도커볼륨(&lt;b&gt;Docker Volume&lt;/b&gt;)&lt;/b&gt;&amp;nbsp;: 도커 컨테이너에서 데이터를 영속적으로 저장하기 위한 방법. 호스트 자체의 저장공간을 공유해 사용하는 형태.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;특정 프로그램을 컨테이너로 띄우고 나서 기능이 추가되면 새로운 이미지를 만들어 컨테이너를 실행시켜야 한다. 이 때 새로운 컨테이너를 통째로 갈아끼우는 방식으로 교체하는데, 그렇게 하면 기존 컨테이너 내부의 데이터도 같이 삭제된다. 따라서 볼륨이라는 개념을 사용해 삭제되면 안되는 데이터를 저장할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;도커볼륨(&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Docker Volume&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;) 사용하기&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;볼륨을 사용할 때는 -v를 입력하고 호스트와 컨테이너의 절대 경로를 다음과 같이 입력해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755681072606&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -v [호스트의 디렉토리 절대경로]:[컨테이너의 디렉토리 절대경로] [이미지명]:[태그명]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;-v : &lt;span style=&quot;text-align: left;&quot;&gt;호스트의 경로(1번)를 빌려서 2번 경로와 공유해서 쓰겠다는 의미.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;[호스트의 디렉토리 절대경로] 에 디렉토리가 이미 존재할 경우, 호스트의 디렉터리가 컨테이너의 디렉터리를 덮어씌운다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이것만 봐서는 어떻게 써야할지 막막한데,&amp;nbsp;Docker로 MySQL를 실행시키는 실습을 통해 볼륨을 사용하는 방법을 자세히 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;패스워드 옵션과 함께 MySQL 실행하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;먼저 mysql을 실행시켰는데, docker ps로 보니 아무것도 떠있지 않다. docker ps -a로 확인해보니 중지된 mysql이 있고, 로그를 살펴보니 패스워드 옵션이 정의되지 않아 실행되지 못한 것이었다. 패스워드 옵션은 아래와 같이 알려주기도 하지만 DockerHub에서 찾을 수 있다.&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;1910&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWTt67/btsP0xNmPoL/gnN9vFfeLcadtbVYloNd71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWTt67/btsP0xNmPoL/gnN9vFfeLcadtbVYloNd71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWTt67/btsP0xNmPoL/gnN9vFfeLcadtbVYloNd71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWTt67%2FbtsP0xNmPoL%2FgnN9vFfeLcadtbVYloNd71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1910&quot; height=&quot;754&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;754&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이 -e 옵션을 추가한 뒤 패스워드 옵션을 주고 다시 실행했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755826734106&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -e MYSQL_ROOT_PASSWORD=password -d -p 3306:3306 mysql&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다시 docker ps로 확인해보니 정상적으로 실행된 것을 알 수 있다.&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;1548&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhK5Ey/btsP3AWZyhW/P2W7LWxuIy4rL6DPAsPy1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhK5Ey/btsP3AWZyhW/P2W7LWxuIy4rL6DPAsPy1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhK5Ey/btsP3AWZyhW/P2W7LWxuIy4rL6DPAsPy1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhK5Ey%2FbtsP3AWZyhW%2FP2W7LWxuIy4rL6DPAsPy1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1548&quot; height=&quot;126&quot; data-origin-width=&quot;1548&quot; data-origin-height=&quot;126&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;컨테이너 안으로 들어가 직접 확인해보니 설정했던 비밀번호인 password가 잘 들어있는 것을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;55&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lsEvE/btsP02U2pHK/5wEr8OASPkDlMuuY2GqCw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lsEvE/btsP02U2pHK/5wEr8OASPkDlMuuY2GqCw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lsEvE/btsP02U2pHK/5wEr8OASPkDlMuuY2GqCw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlsEvE%2FbtsP02U2pHK%2F5wEr8OASPkDlMuuY2GqCw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;55&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;55&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;MySQL 컨테이너로 들어가 데이터베이스 만들기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이제 mysql 컨테이너에서 데이터베이스를 만들고, 컨테이너를 삭제했다가 다시 실행했을 때 해당 데이터베이스가 남아있는지 살펴볼 것이다. 여기서 볼륨이 필요한 이유를 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;컨테이너로 들어가서 mysql -u root -p 명령어로 mysql 내부로 들어간다. 처음 컨테이너를 만들 때 설정한 패스워드를 치고 들어가면 된다.&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;870&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boQtts/btsP0XTNzD8/hmmehrmG1zgBzb0CwR49u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boQtts/btsP0XTNzD8/hmmehrmG1zgBzb0CwR49u1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boQtts/btsP0XTNzD8/hmmehrmG1zgBzb0CwR49u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboQtts%2FbtsP0XTNzD8%2FhmmehrmG1zgBzb0CwR49u1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;870&quot; height=&quot;940&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 mydb라는 새로운 데이터베이스를 만들고 컨테이너를 삭제 후 다시 만들었다. 그리고나서 다시 mysql로 들어가 확인해보니 mydb라는 데이터베이스가 사라졌다. 이런 상황을 방지하기 위해 볼륨을 사용해 컨테이너를 띄울 수 있다.&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;979&quot; data-origin-height=&quot;707&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lh9pM/btsP0PO9bx0/iWuHUYOkAllMw4qbO8kiRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lh9pM/btsP0PO9bx0/iWuHUYOkAllMw4qbO8kiRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lh9pM/btsP0PO9bx0/iWuHUYOkAllMw4qbO8kiRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flh9pM%2FbtsP0PO9bx0%2FiWuHUYOkAllMw4qbO8kiRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;979&quot; height=&quot;707&quot; data-origin-width=&quot;979&quot; data-origin-height=&quot;707&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1755828918419&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 컨테이너 종료
$ docker stop [MySQL 컨테이너 ID]
$ docker rm [MySQL 컨테이너 ID]

# 컨테이너 생성
$ docker run -e MYSQL_ROOT_PASSWORD=password -p 3306:3306 -d mysql
$ docker exec -it [MySQL 컨테이너 ID] bash

$ mysql -u root -p
mysql&amp;gt; show databases; # 아까 생성한 데이터베이스가 없어진 걸 확인할 수 있다.&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;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;볼륨을 활용해 MySQL 띄우기&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위에서 본 도커 볼륨을 사용하는 방법을 다시 보자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755843580964&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -v [호스트의 디렉토리 절대경로]:[컨테이너의 디렉토리 절대경로] [이미지명]:[태그명]&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;실습에서는 다음과 같이 작성했다. docker-mysql 폴더는 미리 만들어두었다. 그리고 pwd 명령어를 통해 출력된 경로를 [호스트의 디렉토리 절대경로]로 둔다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755843541301&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -e MYSQL_ROOT_PASSWORD=password -p 3306:3306 -v /home/won/docker-mysql/data:/var/lib/mysql -d mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;참고로 컨테이너의 디렉토리 절대경로는 DockerHub에서 MySQL을 검색한 후 나오는 공식 문서를 통해 알 수 있다.&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;1597&quot; data-origin-height=&quot;1283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blboJB/btsP1xnLoQp/b1btJeSNKDY2KpI8MMb170/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blboJB/btsP1xnLoQp/b1btJeSNKDY2KpI8MMb170/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blboJB/btsP1xnLoQp/b1btJeSNKDY2KpI8MMb170/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblboJB%2FbtsP1xnLoQp%2Fb1btJeSNKDY2KpI8MMb170%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1597&quot; height=&quot;1283&quot; data-origin-width=&quot;1597&quot; data-origin-height=&quot;1283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;처음 데이터베이스를 만들었을 때와 마찬가지로, 컨테이너를 실행시킨 후 컨테이너 안의 mysql로 들어가 mydb 라는 데이터베이스를 만들어준다.&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;1615&quot; data-origin-height=&quot;969&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NOg0q/btsP1lOneRQ/eNOK8JQ7972CEZL71gtfdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NOg0q/btsP1lOneRQ/eNOK8JQ7972CEZL71gtfdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NOg0q/btsP1lOneRQ/eNOK8JQ7972CEZL71gtfdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNOg0q%2FbtsP1lOneRQ%2FeNOK8JQ7972CEZL71gtfdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1615&quot; height=&quot;969&quot; data-origin-width=&quot;1615&quot; data-origin-height=&quot;969&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그리고 다시 컨테이너를 삭제 후, 다시 컨테이너를 생성해보면 mydb가 남아있는 것을 볼 수 있다. 볼륨을 사용하지 않았을 때는 사라졌었지만, 볼륨을 사용하니 삭제되지 않고 그대로 존재한다.&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;1618&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmyrjd/btsP2whp5aB/5osEUNSOXVhbfwdj0kcFO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmyrjd/btsP2whp5aB/5osEUNSOXVhbfwdj0kcFO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmyrjd/btsP2whp5aB/5osEUNSOXVhbfwdj0kcFO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcmyrjd%2FbtsP2whp5aB%2F5osEUNSOXVhbfwdj0kcFO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1618&quot; height=&quot;715&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;715&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;i&gt;  MYSQL_ROOT_PASSWORD를 바꿔서 명령어를 실행하면?&lt;/i&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;최초로 MySQL 띄울 때 설정한 데이터베이스 비밀번호가 호스트의 저장공간에 저장되기 때문에 컨테이너를 새로 띄워서 값을 바꿨다고 해도 기존의 값이 있어서 바뀌지 않는다. 따라서 비밀번호를 바꾸고 싶다면 MySQL 자체에서 비밀번호를 바꾸는 명령어를 날리거나 처음 저장한 MySQL 데이터 (실습에서는 data 자체)를 지워야 한다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Docker</category>
      <category>docker</category>
      <category>docker volume</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/400</guid>
      <comments>https://3uomlkh.tistory.com/400#entry400comment</comments>
      <pubDate>Fri, 22 Aug 2025 15:48:18 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 도커 주요 명령어 정리</title>
      <link>https://3uomlkh.tistory.com/399</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;인프런 &amp;lt;비전공자도 이해할 수 있는 Docker 입문/실전&amp;gt; 강의를 토대로 정리한 내용입니다.&lt;/i&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;이미지(Image) 관련 명령어&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;이미지 다운로드&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;docker pull [이미지명]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750727700273&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker pull nginx&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;최신 버전 이미지 다운로드. docker&amp;nbsp;pull&amp;nbsp;nginx:latest와&amp;nbsp;동일하게&amp;nbsp;작동&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker pull [이미지명:태그명]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750727883236&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker pull nginx:stable-perl&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;특정 버전 이미지 다운로드. &lt;a style=&quot;color: #000000;&quot; href=&quot;https://hub.docker.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;dockerhub&lt;/a&gt;에서 태그명 확인 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker image ls&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750728040487&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker image ls&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;REPOSITORY : 이미지 이름&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;TAG : 이미지 태그명&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;IMAGE ID : 이미지 ID&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;CREATE : 이미지가 생성된 날짜(다운받은 날짜 X)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;SIZE : 이미지 크기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker image rm [이미지 ID 또는 이미지명]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750728233352&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker image rm nginx&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1750728369208&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker image rm 07a&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;컨테이너에서 사용하고 있지 않은 특정 이미지 삭제&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이미지 ID 입력 시, ID의 일부만 입력 가능&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker image rm -f [이미지 ID 또는 이미지명]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750728331897&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker image rm -f nginx&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;중지된 컨테이너에서 사용하고 있는 특정 이미지 삭제&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;실행 중인 컨테이너에서 사용하고 있는 이미지는 강제 삭제 불가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker image rm $(docker images -q)&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750728528428&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker image rm $(docker images -q)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;컨테이너에서 사용하고 있지 않은 이미지만 전체 삭제&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker image rm -f $(docker images -q)&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750728579815&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker image rm -f $(docker images -q)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;컨테이너에서 사용하고 있는 이미지를 포함해서 전체 이미지 삭제&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt; 컨테이너(Container) 관련 명령어&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;컨테이너 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;생성 / 실행&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; docker create 이미지명[:태그명]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755678307237&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker create nginx&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; docker start 컨테이너명[또는 컨테이너 ID]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755678355692&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker start 컨테이너명[또는 컨테이너 ID]&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; docker run 이미지명[:태그명] &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755678590189&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run nginx&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이미지를 바탕으로 컨테이너 생성 후 실행 (이미지가 없을 시 DockerHub에서 이미지를 다운받아 실행)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;포그라운드에서 실행 &amp;rarr; 추가적인 명령어 조작 불가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;❓ 포그라운드와 백그라운드&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;  포그라운드 : 내가 실행시킨 프로그램의 내용이 화면에서 실행되고 출력되는 상태&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;  백그라운드 : 내가 실행시킨 프로그램이 컴퓨터 내부적으로 실행되는 상태&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; docker run -d --name [컨테이너 이름] 이미지명[:태그명]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755678918869&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -d --name my-web-server nginx&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;컨테이너에 이름 붙여서 생성 및 실행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&amp;nbsp;docker run -d -p [호스트 포트]:[컨테이너 포트] 이미지명[:태그명]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755678981277&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -d -p 4000:80 nginx&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 도커를 실행하는 호스트의 4000번 포트를 컨테이너의 80번 포트로 연결하도록 설정 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;docker ps&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755678412957&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker ps # 실행중인 컨테이너 조회
$ docker ps -a # 모든 컨테이너 조회&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; docker stop 컨테이너명[또는 컨테이너 ID] &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755678515125&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker stop {nginx를 실행시킨 Contnainer ID}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; docker rm 컨테이너명[또는 컨테이너 ID] &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755678540741&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker rm {nginx를 실행시킨 Contnainer ID}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; docker rm -f 컨테이너명[또는 컨테이너 ID] &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755679175917&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker rm -f 컨테이너명{nginx를 실행시킨 Contnainer ID}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; docker rm -f $(docker ps -qa)&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755679278733&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker rm -f $(docker ps -qa)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;중지되어 있는 모든 컨테이너 삭제&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; docker kill 컨테이너명[또는 컨테이너 ID]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755679095909&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker kill {nginx를 실행시킨 Contnainer ID}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;특정 컨테이너 강제 중지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; docker logs [컨테이너 ID 또는 컨테이너명]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755679539766&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -d nginx
$ docker logs [nginx가 실행되고 있는 컨테이너 ID]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot; data-token-index=&quot;0&quot;&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; dokcer logs --tail [로그 끝부터 표시할 줄 수] [컨테이너 ID 또는 컨테이너명]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755679632861&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ dokcer logs --tail 10 [컨테이너 ID 또는 컨테이너명]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot; data-token-index=&quot;0&quot;&gt; 최근 로그 10줄만 조회 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; docker logs -f [컨테이너 ID 또는 컨테이너명]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755679640181&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -d -p 80:80 nginx
$ docker logs -f&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot; data-token-index=&quot;0&quot;&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;docker logs --tail 0 -f [컨테이너 ID 또는 컨테이너명] &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755679661966&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker logs --tail 0 -f [컨테이너 ID 또는 컨테이너명]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot; data-token-index=&quot;0&quot;&gt; 기존 로그는 조회하지 않고 생성되는 로그를 실시간으로 볼 때&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Docker</category>
      <category>docker</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/399</guid>
      <comments>https://3uomlkh.tistory.com/399#entry399comment</comments>
      <pubDate>Mon, 18 Aug 2025 17:42:15 +0900</pubDate>
    </item>
    <item>
      <title>짧은 인턴을 마치고...</title>
      <link>https://3uomlkh.tistory.com/398</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;들어가며&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;부트캠프를 수료하고 나서, 실제 개발 현장을 경험할 수 있는 한 달간의 인턴 기회가 생겼다. 부트캠프에서는 요구사항이 명확히 주어지고 팀원들과 함께 처음부터 만들어가는 구조였다면, 실무는 달랐다. 기존 코드베이스를 파악하고, 외부 시스템과 연동하고, 직접 본 적 없는 사람들과 소통하며 결과물을 만들어야 했다. 기대했던 경험을 모두 얻진 못했지만, 그 과정에서 오히려 더 본질적인 것들을 배웠다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1주차 : 낯선 코드베이스에서 맥락을 읽는 법&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;첫 배정 업무는 커머스 서비스의 기존 코드 분석이었다. Postman으로 API 테스트를 진행하며 전체 데이터 흐름을 파악했고, 코드 구조가 실제 서비스 로직과 어떻게 연결되어 있는지 직접 추적했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;분석 과정에서 한 가지 공백을 발견했다. &lt;b&gt;카테고리 구조가 아직 정의되어 있지 않은 상태&lt;/b&gt;였다. 기획 없이 개발을 진행할 수 없다고 판단해, 직접 카테고리 체계를 설계하고 스프레드시트로 정리한 뒤 컨펌을 받아 진행했다. 개발자가 구현만 담당하는 게 아니라, 요구사항이 불명확한 상황에서 스스로 맥락을 정의하고 진전시켜야 할 때도 있다는 걸 처음 체감한 순간이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;코드 작업으로는 Shopify 기반의 Liquid&amp;middot;JS 코드를 수정해 설계한 카테고리 구조를 반영했고, 기존 코드를 리팩토링하면서 두 가지 원칙을 적용했다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;단일 책임 원칙 (SRP)&lt;/b&gt; : 하나의 메서드가 하나의 역할만 하도록 분리해, 변경 영향 범위를 좁혔다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이론으로 배웠던 원칙들을 실제 코드에 적용하는 작업은 생각보다 훨씬 판단이 많이 필요한 일이었다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2~4주차 : 외부 API 연동 백엔드 개발&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;중반부터는 두 개의 외부 플랫폼 API를 연동하는 백엔드 개발을 맡았다. 부트캠프에서는 외부 API를 직접 다룬 경험이 없었기 때문에, 공식 문서를 꼼꼼히 읽고 Postman으로 동작을 검증하면서 연동 방식을 파악하는 것부터 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;API 명세서 : 구현 전에 먼저 정의한다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;혼자 백엔드를 담당하는 구조였기 때문에 API 명세도 직접 처음부터 작성했다. 총 &lt;b&gt;29개의 API&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;어떤 데이터를 응답에 포함시켜야 하는가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;에러 케이스는 어떤 형태로 내려줘야 하는가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;명세는 마지막 주까지 피드백을 받으며 수정했다. 이 과정에서 명확한 문서가 단순히 &quot;기록&quot;이 아니라, 협업 속도와 커뮤니케이션 비용을 직접 결정한다는 걸 체감했다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;구현한 주요 기능&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; API&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 주요 처리 내용 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;토큰 저장 시 유저 정보 DB 저장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;외부 API 3개 호출로 사용자 정보&amp;middot;스토어명&amp;middot;활성 상품 수 조회 후 저장&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스토어 목록 조회&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;토큰 만료일 포함 응답 처리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;상품 조회&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;외부 API 연동 및 응답 가공&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;중복 제거 : ArgumentResolver + 커스텀 어노테이션 적용&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;구현 과정에서 기존 코드의 구조적 문제를 발견했다. 토큰 추출 로직이 컨트롤러의 모든 메서드에 동일하게 반복되고 있었다. 단순 중복인 것처럼 보이지만, 이 구조에서는 추출 방식이 바뀌면 모든 메서드를 일일이 수정해야 했다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;수정 방식보다 구조를 바꾸는 게 맞다고 판단해, Spring MVC의 &lt;b&gt;ArgumentResolver&lt;/b&gt;를 활용하는 방향으로 개선했다. 컨트롤러 메서드 파라미터에 &lt;b&gt;@EbayToken&lt;/b&gt; 어노테이션만 붙이면 토큰 추출이 자동으로 처리되도록 하고, 이 ArgumentResolver를 Spring MVC에 등록해 모든 컨트롤러에서 재사용할 수 있게 했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 개선 전: 토큰 추출 로직이 모든 메서드에 반복
String token = request.getHeader(&quot;Authorization&quot;).replace(&quot;Bearer &quot;, &quot;&quot;);

// 개선 후: 어노테이션만 붙이면 Spring MVC가 자동으로 처리
public ResponseEntity&amp;lt;?&amp;gt; getItems(@EbayToken String token) { ... }
&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;변경 이후에는 토큰 처리 로직이 한 곳에서만 관리되기 때문에, 이후 수정이 생기더라도 ArgumentResolver 하나만 고치면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;외부 클라이언트 소통&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;혼자 백엔드를 담당하다 보니, 직접 외부 클라이언트와 소통할 일도 생겼다. 계정 인증 이슈나 API 계정 발급 요청 등을 전화&amp;middot;메시지&amp;middot;메일로 처리했다. 업무 맥락에서 외부와 소통하는 것이 처음이라 처음에는 어색했지만, 기술적인 상황을 상대방이 이해할 수 있는 언어로 전달하는 연습이 됐다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;돌아보며 : 한 달이 남긴 것&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기대했던 것과 달랐던 점, 그래서 배운 것&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;프론트엔드와 처음 협업하면서 인증 토큰 처리를 내부에서 해줬어야 하는 부분을 초기에 놓쳤다. 경험 부족에서 비롯된 실수였지만, 이후 API를 설계할 때 &quot;호출하는 쪽이 신경 써야 하는 것을 최소화하자&quot;는 원칙을 의식적으로 적용하게 된 계기가 됐다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;실무에서 다시 확인한 것들&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;외부 API 연동은 문서 읽기에서 시작된다.&lt;/b&gt; 잘 모르는 시스템을 다룰 때 가장 먼저 해야 할 일은 공식 문서를 제대로 읽고, 직접 요청을 날려보며 동작을 확인하는 것이다. 이 순서가 익숙해지면 어떤 외부 시스템이든 처음의 막막함이 줄어든다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;문서화는 커뮤니케이션 비용을 결정한다.&lt;/b&gt; 명세가 명확할수록 질문이 줄고, 구현 속도가 빨라진다. 29개의 API 명세를 작성하면서 좋은 문서는 팀의 맥락을 공유하는 가장 효율적인 방법이라는 걸 느꼈다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;리팩토링은 코드를 고치는 게 아니라 구조를 바꾸는 것이다.&lt;/b&gt; 단순히 중복을 제거하는 것을 넘어, 변경이 생겼을 때 영향 범위를 최소화하는 구조를 고민하는 것이 진짜 리팩토링이라는 걸 실제 코드를 통해 배웠다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;마치며&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;한 달은 짧았다. 그렇지만 실제 서비스 코드를 분석하고, 외부 API를 직접 연동하고, 프론트엔드와 명세를 주고받으며 다듬어가는 경험은 부트캠프와는 결이 달랐다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;가장 크게 남은 질문은 이것이다. &lt;b&gt;&quot;내가 짠 코드는 유지보수하기 좋은 코드인가?&quot;&lt;/b&gt; 혼자 작성하고 검증받지 못한 코드를 보면서, 코드 리뷰가 단순한 오류 수정이 아니라 더 나은 설계를 함께 만드는 과정이라는 걸 더 절실하게 이해하게 됐다. 다음에는 그 부분을 적극적으로 만들어가고 싶다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>회고</category>
      <category>인턴</category>
      <category>취업</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/398</guid>
      <comments>https://3uomlkh.tistory.com/398#entry398comment</comments>
      <pubDate>Fri, 8 Aug 2025 20:09:08 +0900</pubDate>
    </item>
    <item>
      <title>[STUDY] 데이터베이스 핵심 개념 정리 : 정규화 &amp;middot; 인덱스 &amp;middot; 트랜잭션</title>
      <link>https://3uomlkh.tistory.com/397</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; ️ 데이터 베이스&lt;/span&gt;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터베이스(database) : 사용자나 프로그램에서 사용하기 위해 저장 및 관리하는 데이터 집합&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;개체(entity) : 데이터로 표현하려는 대상&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;속성(attribute) : 개체의 특성과 상태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;관계(relationship) : 개체 간에 어떤 관련이 있는지 나타냄&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;관계형 데이터베이스&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;관계형 데이터베이스(RDB, Relational Database) : &amp;nbsp;데이터가 2차원 테이블에 저장되며, 데이터의 구조와 데이터 간 종속성 등을 알 수 있는 데이터베이스&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;릴레이션 : 관계형 데이터베이스에서 데이터의 개념적 모델&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;테이블 : 릴레이션을 실제로 구현한 개체로, 키-값으로 이루어진 2차원 형태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;행(=튜플, 레코드) : 테이블을 구성하는 데이터들 중 가로로 묶은 데이터셋&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;열(=속성, 필드) : 테이블을 구성하는 데이터들 중 세로로 묶은 데이터셋&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;NoSQL 데이터베이스&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;NoSQL(Not Only SQL) 데이터베이스 : 비관계형 데이터베이스로, 보다 유연한 형태의 데이터베이스&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;대용량 데이터 조회시 속도 면에서 유리하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;수평적 확장성이 좋아 서버를 추가해 데이터베이스를 확장하는 것이 쉽다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;관계형 데이터베이스 vs &lt;/b&gt;&lt;b&gt;NoSQL 데이터베이스&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.0775%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.7054%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;관계형 데이터베이스&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;NoSQL 데이터베이스&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.0775%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터 저장&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.7054%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2차원 테이블로 저장&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;분산형 구조로, 도큐먼트, 키-값, 그래프 등 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다양한 형태로 저장&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.0775%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터 중복&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.7054%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;중복 없음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;중복 데이터가 있어서 데이터 갱신 시 이를 처리해야 함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.0775%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스키마&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.7054%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스키마가 있어 데이터의 무결성 보장&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스키마가 없고 유연한 데이터 처리 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.0775%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터 확장&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.7054%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;수직적 확장 지원&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;수평적 확장 지원&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.0775%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DBMS&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.7054%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Oracle, MySQL&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;MongoDB, Redis&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;  인덱스&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;인덱스&lt;/b&gt; : 추가적인 쓰기 작업과 저장 공간을 활용해 데이터베이스에서 튜플의 검색 성능을 높이기 위한 자료구조. 주로 조회 조건에 자주 사용되는 컬럼(WHERE절), JOIN에 자주 사용되는 컬럼, 정렬이나 GROUP BY에 사용되는 컬럼에 인덱스를 건다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스는 데이터베이스의 성능을 향상시키기 위해 일반적으로 사용되는 방법이다. 키-값 형태로 '속성 값 - 튜플 주소'를 인덱스 테이블에 저장한다. 인덱스 테이블은 다음처럼 속성값을 기준으로 정렬 상태를 유지한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3322&quot; data-origin-height=&quot;1184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dioXWw/btsOSNJ7CYg/l9K9pkW1ldQNKkDlhPosAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dioXWw/btsOSNJ7CYg/l9K9pkW1ldQNKkDlhPosAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dioXWw/btsOSNJ7CYg/l9K9pkW1ldQNKkDlhPosAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdioXWw%2FbtsOSNJ7CYg%2Fl9K9pkW1ldQNKkDlhPosAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3322&quot; height=&quot;1184&quot; data-origin-width=&quot;3322&quot; data-origin-height=&quot;1184&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스의 장단점을 정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스 테이블에 데이터가 정렬되어 있어서 검색 속도가 빠르다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인덱스 테이블을 저장하기 위한 추가 공간이 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;정렬된 상태를 유지하기 위해 데이터를 추가, 수정, 삭제하는 경우 속도가 느리다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해시 테이블(hash table) : 속성 값으로 해시 값을 계산해 인덱싱하는 방법&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;O(1) 수준의 빠른 탐색&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;캐시 효율이 좋아 메모리 엔진 등에 적합&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;범위 검색, 정렬이 필요한 연산 불가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해시 충돌 발생 시 성능 저하 가능성&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;B+ 트리&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;B+ 트리(Balanced Tree) : 단말 노드에만 데이터를 저장하고 단말 노드 간에는 연결 리스트로 연결되는 방식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; MySQL, PostgreSQL, Oracle 등 대부분의 DBMS의 기본 인덱스 구조 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;정렬된 구조를 유지해 범위 검색에 강함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;O(log n) 탐색 시간&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;삽입/삭제 시 노드 분할 또는 병합이 필요해 해시보다 약간 느림&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;트랜잭션이란&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;트랜잭션(transaction) : 데이터베이스의 상태를 바꾸기 위해 수행하는 작업의 단위 또는 일련의 연산&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;ACID 특성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;원자성(Atomicity) : 트랜잭션이 데이터베이스에 완전히 반영되거나 아예 실행되지 않아야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;일관성(Consistency) : 트랜잭션 수행이 완료된 데이터베이스는 일관성이 있다.(데이터 무결성 유지)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;독립성(Isolation) : 수행 중인 트랜잭션에 다른 트랜잭션이 끼어들 수 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;영속성(Durability) : 완료한 트랜잭션의 결과가 데이터베이스에 영구적으로 반영된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;데드락&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데드락(deadlock) : 두 개 이상의 트랜잭션이 서로가 점유한 자원을 기다리면서 무한 대기 상태에 빠지는 현상&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;트랜잭션 수행 전 미리 락 획득&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;트랜잭션이 들어온 순서에 따라 락을 획득하거나 특랜잭션을 종료&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;이상&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이상(anomaly) : 트랜잭션 처리 중 속성 간 종속이나 데이터 중복으로 발생하는 문제&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;삽입 이상(insertion anomaly) : 데이터 삽입 시 의도치 않은 다른 데이터도 삽입된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;갱신 이상(update anomaly) : 데이터 갱신 시 일부 튜플만 갱신되어 데이터 모순이 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;삭제 이상(deletion anomaly) : 데이터 삭제 시 의도하지 않은 데이터도 삭제된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;정규화 단계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;정규화(normalization) :&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터의 일관성, 최소한의 데이터 중복, 최대한의 데이터 유연성을 위한 방법&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;테이블을 분해하느라 연산 시간 증가 &amp;rarr;상황에 따른 적절한 정규화 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4bJwL/btsOT8UP19l/P8KagVLqxFVejDW2NjyIJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4bJwL/btsOT8UP19l/P8KagVLqxFVejDW2NjyIJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4bJwL/btsOT8UP19l/P8KagVLqxFVejDW2NjyIJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4bJwL%2FbtsOT8UP19l%2FP8KagVLqxFVejDW2NjyIJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1362&quot; height=&quot;1160&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;제1정규형(1NF, First Normal Form)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;제1정규형은 &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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 예를 들어, 전화번호가 이렇게 쉼표로 두 개가 들어가 있으면 쿼리를 짜기도 어렵고, 데이터 분석이 힘들어진다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 26.1628%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 학생이름&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 73.8372%;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; &amp;nbsp;전화번호 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 26.1628%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;홍길동&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 73.8372%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;010-1111-2222, 010-3333-4444&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 '전화번호' 속성에 속성 값이 하나만 존재할 수 있도록 다음고 같이 테이블을 분해한다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 63px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 26.3954%; text-align: center; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 학생이름 &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.4884%; text-align: center; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 전화번호 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 26.3954%; text-align: center; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;홍길동&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.4884%; text-align: center; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;010-1111-2222&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 26.3954%; text-align: center; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;홍길동&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.4884%; text-align: center; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;010-3333-4444&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;제2정규형(2NF, Second Normal Form)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;제2정규형은 &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;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;예를 들어, 아래와 같이 홍길동이 과목을 여러 개 들을 때마다 이름이 계속 중복되고 있다. 여기서 학생이름은 학생 ID에만 종속되므로 부분 종속되어 있다고 할 수 있다. 이 경우 제2정규형을 위반하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 학생ID &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 과목ID &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 점수 &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 학생이름 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;A&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;90&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;홍길동&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;B&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;85&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;홍길동&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;A&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;88&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;김철수&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; &amp;zwj;  학생 테이블&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 학생ID &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 학생이름 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;홍길동&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;김철수&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;  수강 테이블&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 학생ID &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 과목ID &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 점수 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;A&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;90&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;B&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;85&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;A&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;88&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; 제3정규형(3NF, Third Normal Form)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 제3정규형은 &lt;b&gt;제2정규형을 만족하고, 이행적 종속이 없어야 한다.&amp;nbsp;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&amp;nbsp;아래 테이블을 보면 부서 이름은 부서 ID에 종속되고, 부서 ID는 직원 ID에 종속되어있다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 82px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 직원ID &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 부서ID &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 부서이름 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인사팀&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;20&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;개발팀&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인사팀&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; &amp;zwj; 직원 테이블&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 직원ID &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 부서ID &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;20&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;  부서 테이블&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 56px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 14px;&quot;&gt;
&lt;td style=&quot;height: 14px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 부서ID &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 14px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 부서이름 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인사팀&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;20&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;개발팀&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;역정규화&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;역정규화(denormalization ) : 정규화된 테이블을 낮은 정규화 단계의 테이블로 되돌리는 것&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;  역정규화를 하는 이유?&lt;br /&gt;&lt;/b&gt;정규화는 데이터 중복을 제거하고 정합성을 높이는 데 효과적이지만, 테이블이 지나치게 분리되면 조인이 많아져 조회 성능이 떨어질 수 있다. 따라서 조회 성능이 중요한 상황에서는 일규 정규화 구조를 풀고 중복을 허용해 쿼리 성능을 개선하기 위해의도적으로 역정규화를 적용한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  참고자료&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;figure id=&quot;og_1750930109225&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;기술 면접 대비 CS 전공 핵심요약집 | 이수진 - 교보문고&quot; data-og-description=&quot;기술 면접 대비 CS 전공 핵심요약집 | 1분 1초가 아쉬운 취업/이직 준비생을 위해 기술 면접에 필요한 CS 전공의 핵심 내용만 간추렸다!비전공자는 물론 IT 전공자라도 광범위한 CS 전공 중에 어떤 &quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000208504237&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000208504237&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ZZI7P/hyZcqFjJbl/cP1WQhrmxHTr5nPpKTUW10/img.jpg?width=458&amp;amp;height=589&amp;amp;face=0_0_458_589,https://scrap.kakaocdn.net/dn/EVRE0/hyZcfX5yO6/HQcKIMKxRIhJCusd8yPklk/img.jpg?width=458&amp;amp;height=589&amp;amp;face=0_0_458_589,https://scrap.kakaocdn.net/dn/cMilVE/hyZcjlTux8/cYCWkYUV5hisf74uXKgsx0/img.jpg?width=814&amp;amp;height=4538&amp;amp;face=0_0_814_4538&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000208504237&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000208504237&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ZZI7P/hyZcqFjJbl/cP1WQhrmxHTr5nPpKTUW10/img.jpg?width=458&amp;amp;height=589&amp;amp;face=0_0_458_589,https://scrap.kakaocdn.net/dn/EVRE0/hyZcfX5yO6/HQcKIMKxRIhJCusd8yPklk/img.jpg?width=458&amp;amp;height=589&amp;amp;face=0_0_458_589,https://scrap.kakaocdn.net/dn/cMilVE/hyZcjlTux8/cYCWkYUV5hisf74uXKgsx0/img.jpg?width=814&amp;amp;height=4538&amp;amp;face=0_0_814_4538');&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;기술 면접 대비 CS 전공 핵심요약집 | 이수진 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;기술 면접 대비 CS 전공 핵심요약집 | 1분 1초가 아쉬운 취업/이직 준비생을 위해 기술 면접에 필요한 CS 전공의 핵심 내용만 간추렸다!비전공자는 물론 IT 전공자라도 광범위한 CS 전공 중에 어떤&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1750930117999&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;  데이터베이스의 특징에 대해 설명해주세요. 실시간 접근성(Real-Time Accessibility) : 비정형적인 질의(조회)에 대하여 실시간 처리에 의한 응답이 가능해야 하며, 지속적인 변화(Continuous Evloution) :&quot; data-og-host=&quot;dev-coco.tistory.com&quot; data-og-source-url=&quot;https://dev-coco.tistory.com/158&quot; data-og-url=&quot;https://dev-coco.tistory.com/158&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eefphI/hyZbDY6gJO/oSOqjcTmVmVwA1VO23b6i0/img.png?width=602&amp;amp;height=244&amp;amp;face=0_0_602_244,https://scrap.kakaocdn.net/dn/bcl5gk/hyZcjGcqlg/3jq5rdaimzuAdMI4L4WckK/img.png?width=602&amp;amp;height=244&amp;amp;face=0_0_602_244,https://scrap.kakaocdn.net/dn/bSmawp/hyZccAjLMP/bRi8CB5hAEtFC3noJR4xw1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot;&gt;&lt;a href=&quot;https://dev-coco.tistory.com/158&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev-coco.tistory.com/158&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eefphI/hyZbDY6gJO/oSOqjcTmVmVwA1VO23b6i0/img.png?width=602&amp;amp;height=244&amp;amp;face=0_0_602_244,https://scrap.kakaocdn.net/dn/bcl5gk/hyZcjGcqlg/3jq5rdaimzuAdMI4L4WckK/img.png?width=602&amp;amp;height=244&amp;amp;face=0_0_602_244,https://scrap.kakaocdn.net/dn/bSmawp/hyZccAjLMP/bRi8CB5hAEtFC3noJR4xw1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720');&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;  데이터베이스의 특징에 대해 설명해주세요. 실시간 접근성(Real-Time Accessibility) : 비정형적인 질의(조회)에 대하여 실시간 처리에 의한 응답이 가능해야 하며, 지속적인 변화(Continuous Evloution) :&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev-coco.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1750930147914&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - WooVictory/Ready-For-Tech-Interview:   신입 개발자로서 지식을 쌓기 위해 공부하는 공간  &amp;zwj;&quot; data-og-description=&quot;  신입 개발자로서 지식을 쌓기 위해 공부하는 공간  &amp;zwj; . Contribute to WooVictory/Ready-For-Tech-Interview development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/WooVictory/Ready-For-Tech-Interview&quot; data-og-url=&quot;https://github.com/WooVictory/Ready-For-Tech-Interview&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/A6Iaa/hyZckkPtHB/h7tpzH2h2LCfxlIW3PBvhK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bn6gcE/hyZclcVAkg/uD5T9nPvYm37dFCjS3HMB0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/WooVictory/Ready-For-Tech-Interview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/WooVictory/Ready-For-Tech-Interview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/A6Iaa/hyZckkPtHB/h7tpzH2h2LCfxlIW3PBvhK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bn6gcE/hyZclcVAkg/uD5T9nPvYm37dFCjS3HMB0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - WooVictory/Ready-For-Tech-Interview:   신입 개발자로서 지식을 쌓기 위해 공부하는 공간  &amp;zwj;&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  신입 개발자로서 지식을 쌓기 위해 공부하는 공간  &amp;zwj; . Contribute to WooVictory/Ready-For-Tech-Interview development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>DB</category>
      <category>기술면접</category>
      <category>데이터베이스</category>
      <category>인덱스</category>
      <category>정규화</category>
      <category>트랜잭션</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/397</guid>
      <comments>https://3uomlkh.tistory.com/397#entry397comment</comments>
      <pubDate>Thu, 26 Jun 2025 18:29:23 +0900</pubDate>
    </item>
    <item>
      <title>[STUDY] Spring IoC/DI와 객체지향 설계 원칙</title>
      <link>https://3uomlkh.tistory.com/396</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;⭐ 객체지향 설계가 중요한 이유&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;객체 지향 프로그래밍은 객체를 중심으로 시스템을 구성하는 방식이다. 현실 세계의 사물이나 사건을 하나의 객체로 보고, 이런 객체들 간의 상호작용을 중심으로 프로그래밍한다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;복잡한 시스템일수록 기능을 분리하고, 역할을 명확히 나눈 구조가 필요하다. &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;객체지향은 이러한 확장성과 유지보수성을 높이기 위한 핵심 설계 방식이다. &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Spring은 객체지향 설계를 전제로 만들어진 프레임워크로, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;특히 IoC와 DI는 객체 간의 관계를 유연하게 구성하고 관리하는 데 중요한 역할을 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;  OOP 특징과 설계 원칙&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;객체 지향의 4가지 핵심 개념&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;추상화( Abstraction )&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;불필요한 세부 사항을 감추고 핵심적인 동작이나 속성만 드러내는 것&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;List 인터페이스를 사용할 때 내부 구조는 몰라도 add()나 get() 같은 기능만 사용하면 된다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;캡슐화( Encapsulation )&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;객체의 내부 상태를 감추고, 외부에서는 정해진 방식으로만 접근하게 하는 것&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;필드를 private으로 감추고 getter/setter를 사용하는 방식으로 캡슐화할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 추상화와 캡슐화는 모두 '정보 은닉'을 기반으로 하지만, 추상화는 '무엇을 제공할지', 캡슐화는 '무엇을 감출지'에 초점을 둔다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상속( Inheritance )&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 기존 클래스를 확장하여 새로운 클래스를 정의하는 것 &lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;공통 기능을 부모 클래스에 정의해두고, 자식 클래스는 이를 물려받아 재사용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다형성( Polymorphism )&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 한 객체가 여러 타입의 객체로 취급될 수 있는 능력&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다형성을 활용하면 중복을 제거하고, 변경사항이 생겨도 코드를 크게 고치지 않고 재사용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;  이전에 자바 강의를 보며 다형성에 대해 공부하고 정리한 내용이다.&lt;/span&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750324248211&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;[Java/Study] 김영한의 실전 자바 기본 - 스터디 5회차&quot; data-og-description=&quot;인프런 강의 을 보고 정리한 내용입니다.매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.다형성(Polymorphism)객체 지향 프로그래밍의 특징 중 하나인 다형성은&amp;nbsp;한 &quot; data-og-host=&quot;3uomlkh.tistory.com&quot; data-og-source-url=&quot;https://3uomlkh.tistory.com/326&quot; data-og-url=&quot;https://3uomlkh.tistory.com/326&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/OSgoJ/hyZbBZVZW6/0BXemvjHQ1F8RplzBTLSZk/img.png?width=721&amp;amp;height=481&amp;amp;face=0_0_721_481,https://scrap.kakaocdn.net/dn/b3SR1s/hyY8Qj2DkK/jFjnPy6JwV9NXp12cxBXbK/img.png?width=721&amp;amp;height=481&amp;amp;face=0_0_721_481,https://scrap.kakaocdn.net/dn/bixUWj/hyY8S3cI3j/eXXPeLNVseAAdFhoqjCCq0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://3uomlkh.tistory.com/326&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://3uomlkh.tistory.com/326&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/OSgoJ/hyZbBZVZW6/0BXemvjHQ1F8RplzBTLSZk/img.png?width=721&amp;amp;height=481&amp;amp;face=0_0_721_481,https://scrap.kakaocdn.net/dn/b3SR1s/hyY8Qj2DkK/jFjnPy6JwV9NXp12cxBXbK/img.png?width=721&amp;amp;height=481&amp;amp;face=0_0_721_481,https://scrap.kakaocdn.net/dn/bixUWj/hyY8S3cI3j/eXXPeLNVseAAdFhoqjCCq0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Java/Study] 김영한의 실전 자바 기본 - 스터디 5회차&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;인프런 강의 을 보고 정리한 내용입니다.매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.다형성(Polymorphism)객체 지향 프로그래밍의 특징 중 하나인 다형성은&amp;nbsp;한&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;3uomlkh.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SOLID 원칙&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;SRP ( 단일 책임 원칙 )&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하나의 클래스는 하나의 책임만 가져야 한다.&lt;/span&gt;&lt;/blockquote&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;OCP ( 개방-폐쇄 원칙 )&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;확장에는 열려 있고, 변경에는 닫혀 있어야 한다.&lt;/span&gt;&lt;/blockquote&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;LSP ( 리스코프 치환 원칙 )&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자식 클래스는 부모 클래스를 대체할 수 있어야 한다.&lt;/span&gt;&lt;/blockquote&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;ISP ( 인터페이스 분리 원칙 )&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;클라이언트에 맞는 인터페이스를 분리해 제공해야 한다.&lt;/span&gt;&lt;/blockquote&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DIP ( 의존 역전 원칙 )&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안 된다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;  IoC와 DI 이해하기 &lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;제어의 역전( Inversion of Control, IoC)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것.&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;객체 생성이나 의존성 관리 같은 책임을 프레임워크가 대신 담당하는 개념이다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; ️ IoC란 무엇인가요? 왜 필요한가요?&lt;/b&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; IoC는 제어의 역전(Inversion of Control)을 뜻하며, 객체 생성이나 의존성 관리 같은 책임을 개발자가 아니라 프레임워크가 대신 담당하는 개념입니다. Spring에서는 IoC 컨테이너가 객체를 생성하고, 필요한 의존성을 주입해줍니다. 이로 인해 객체 간 결합도가 낮아지고, 테스트나 유지보수가 용이해집니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;의존성 주입( Dependency Injection, DI)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 객체 간의 의존 관계를 외부에서 주입받는 방식&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring에서는 생성자, 필드, 세터 주입 방식이 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; DI를 통해 구현체가 바뀌어도 클라이언트 코드는 변경되지 않아, 결합도를 낮추고 테스트가 쉬워진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; ️ DI(의존성 주입)는 왜 필요한가요? 직접 new로 만들면 안 되나요? &lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 직접 new로 객체를 생성하면 클래스 내부에서 구현체에 강하게 결합됩니다. 반면 DI는 외부에서 의존성을 주입받아 추상화에 의존하게 만듭니다. 이를 통해 DIP(의존 역전 원칙)를 지킬 수 있고, 구현체 교체나 테스트가 유연해집니다. 예를 들어, 실제 서비스 대신 Mock 객체를 주입해 테스트할 수 있습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;아래 코드는 &amp;lt;스프링 핵심 원리 - 기본편&amp;gt;에 나오는 예시 중 일부인데, 직접 구현체에 의존하는 구조여서 잘못된 예시로 가져왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;OrderServiceImpl이 discountPolish 인터페이스 뿐만 아니라 RateDiscountPolish인 구체 클래스도 함께 의존하고있다. (DIP 위반) 따라서 할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다. (OCP 위반)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750325369144&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolish discountPolish = new RateDiscountPolish();
    
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolish.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;270&quot; data-start=&quot;146&quot; data-ke-size=&quot;size16&quot;&gt;예시에서는 AppConfig 클래스를 통해 애플리케이션에 필요한 구현 객체를 생성하고, 생성자를 통해 의존성을 주입했다.&lt;br /&gt;이처럼 객체를 생성하고 의존 관계를 연결하는 구성을 IoC 컨테이너 또는 DI 컨테이너라고 한다.&lt;/p&gt;
&lt;p data-end=&quot;485&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;AppConfig는 구현체 변경 시 해당 설정만 수정하면 되므로, 클라이언트 코드의 변경 없이 유연하게 확장할 수 있다.&lt;br /&gt;다만, 이는 개념 설명을 위한 수동 설정 예시이며, 실제 Spring Boot 환경에서는 @ComponentScan과 생성자 주입(@Autowired 또는 Lombok)을 통해 자동 주입이 이뤄지므로 AppConfig를 직접 작성할 필요는 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1750325541045&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public static MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolish());
    }

    @Bean
    public DiscountPolish discountPolish() {
        return new RateDiscountPolish();
    }
}&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; OrderServiceImpl은 추상화에만 의존하도록 변경되었고, 이를 통해 DIP와 OCP 원칙을 만족하며, DI와 IoC 구조도 함께 실현된다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750325778835&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolish discountPolish;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolish discountPolish) {
        this.memberRepository = memberRepository;
        this.discountPolish = discountPolish;
    }

    // ...
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DI</category>
      <category>IoC</category>
      <category>SOLID</category>
      <category>Spring</category>
      <category>객체지향</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/396</guid>
      <comments>https://3uomlkh.tistory.com/396#entry396comment</comments>
      <pubDate>Thu, 19 Jun 2025 18:39:42 +0900</pubDate>
    </item>
    <item>
      <title>[STUDY] Spring Batch 핵심 구조와 개념 정리</title>
      <link>https://3uomlkh.tistory.com/395</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;  Spring Batch란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; 배치(Batch) 처리&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 매일 자정 주문 데이터 통계 저장 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;매주 고객 대상 이메일 발송&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DB 마이그레이션, 정산 처리 등&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;사람이 직접 요청하지 않아도 자동으로 주기적 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;실시간성보다 정확성과 안정성이 중요&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Spring Batch&lt;/b&gt;는 대용량 배치 작업을 쉽고 안전하게 처리할 수 있도록 도와주는 Spring 기반 프레임워크다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Job/Step 기반 구성 제공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;예외, 로그. 재시작, 로깅 등의 기능 제공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;청크 기반 처리, 병렬 처리 등 내장&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;  Spring Batch의 구조&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Job과 Step&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;아래 그림은 Spring Batch의 전반적인 실행 흐름을 나타낸다.&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;1442&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBrai9/btsOu73lgDx/B933HmCyVHgO6KmvcbwX10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBrai9/btsOu73lgDx/B933HmCyVHgO6KmvcbwX10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBrai9/btsOu73lgDx/B933HmCyVHgO6KmvcbwX10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBrai9%2FbtsOu73lgDx%2FB933HmCyVHgO6KmvcbwX10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1442&quot; height=&quot;648&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;JobLauncher&lt;/b&gt; : 배치 실행을 트리거하는 컴포넌트. 배치 실행 시 JobRepository에 JobExecution을 생성해 기록 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Job&lt;/b&gt; : 하나의 배치 단위. 여러 Step으로 구성될 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Step&lt;/b&gt; : 실제&amp;nbsp;데이터&amp;nbsp;처리가&amp;nbsp;수행되는&amp;nbsp;단위.&amp;nbsp;내부적으로&amp;nbsp;Reader&amp;nbsp;&amp;rarr;&amp;nbsp;Processor&amp;nbsp;&amp;rarr;&amp;nbsp;Writer&amp;nbsp;순서로&amp;nbsp;처리됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;JobRepository&lt;/b&gt; : Job/Step의 실행 상태를 저장하며, 재시작/중단/성공 여부 등을 추적&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; ItemReader&lt;/b&gt; : 데이터 소스로부터 입력 (CSV, DB, API 등) &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; ItemProcessor&lt;/b&gt; : 읽은 데이터를 가공, 변환, 필터링 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; ItemWriter&lt;/b&gt; : 최종 데이터를 외부로 출력 (DB 저장, 파일 기록 등)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; ️ Spring Batch의 Job과 Step의 구조에 대해 설명해주세요.&lt;/b&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring Batch는 Job과 Step이라는 실행 단위로 구성되어 있습니다. Job은 배치 작업의 전체 단위를 나타내며, 여러 개의 Step으로 구성될 수 있습니다. 각 Step은 Reader, Processor, Writer 구성 요소를 포함하며 실제 데이터 처리가 수행되는 단위입니다. Job을 실행하면 JobLauncher가 실행을 시작하고, JobRepository에 실행 정보가 저장되어 상태 추적이나 재시작이 가능하도록 관리됩니다. Step은 각각 독립적으로 트랜잭션을 가질 수 있고, 순차적으로 실행하거나 조건에 따라 분기할 수도 있습니다. 이러한 구조는 작업을 단계별로 나누고 실행 흐름과 상태를 명확히 관리할 수 있도록 해줍니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특히 실행 실패 시 특정 Step부터 다시 실행하는 구조를 지원해, 복구 가능성과 실행 제어를 용이하게 합니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Reader&amp;nbsp;&amp;middot;&amp;nbsp;Processor &amp;nbsp;&amp;middot;&amp;nbsp; Writer&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Spring Batch에서 하나의 Step은 크게 세 가지 구성 요소로 이루어진다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Reader&lt;/b&gt; (ItemReader) &amp;ndash; 데이터를 읽는 구성 요소&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 외부에서 데이터를 읽어오는 역할 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 입력 소스는 CSV, DB, XML, API 등 다양하게 설정 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;한 번에 하나의 데이터를 반환, 더 이상 읽을 데이터가 없으면 null 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Processor&lt;/b&gt; (ItemProcessor) &amp;ndash; 데이터를 가공하는 구성 요소&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; Reader에서 읽어온 데이터를 가공하거나 변환하는 역할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;반환값이 null일 경우 해당 데이터는 writer에 전달되지 않음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 필수 구성 요소는 아니며, 필요 시에만 사용 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Writer&lt;/b&gt; (ItemWriter) &amp;ndash; 데이터를 저장하는 구성 요소&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 처리된 데이터를 최종적으로 어떤 형태로 저장할지 정하고, 외부에 출력하는 역할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; DB에 저장하거나, 파일로 출력하거나, 외부 API로 전송하는 등의 작업을 수행 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Reader와 달리 청크 단위로 데이터를 전달받아 처리&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 아래 코드는 팀 프로젝트에서 사용된 정산 처리 Step의 예시다. Settlement 데이터를 읽고, 상태를 변경한 뒤 DB에 반영하는 단일 Step으로, chunk(100)으로 설정된 만큼 데이터를 모아 한 번에 처리하고 커밋한다.&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;1207&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QqXWA/btsOuux2Cxq/aGftoKCwsp5EZb7yuGJydk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QqXWA/btsOuux2Cxq/aGftoKCwsp5EZb7yuGJydk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QqXWA/btsOuux2Cxq/aGftoKCwsp5EZb7yuGJydk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQqXWA%2FbtsOuux2Cxq%2FaGftoKCwsp5EZb7yuGJydk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1207&quot; height=&quot;468&quot; data-origin-width=&quot;1207&quot; data-origin-height=&quot;468&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Step은 Reader &amp;rarr; Processor &amp;rarr; Writer 구조로 이루어져 있으며, 각 구성 요소는 다음과 같은 역할을 한다.&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: #333333; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; Reader : status가 'READY'인 정산 데이터를 조회(커서 기반)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Processor : 상태를 'COMPLETED'로 변경 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Writer : 변경된 상태를 DB에 반영 (UPDATE) &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;1435&quot; data-origin-height=&quot;1287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdwKi9/btsOt7wC9Cn/FcJXS9Jx9dD3XbgE21a1uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdwKi9/btsOt7wC9Cn/FcJXS9Jx9dD3XbgE21a1uK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdwKi9/btsOt7wC9Cn/FcJXS9Jx9dD3XbgE21a1uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdwKi9%2FbtsOt7wC9Cn%2FFcJXS9Jx9dD3XbgE21a1uK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1435&quot; height=&quot;1287&quot; data-origin-width=&quot;1435&quot; data-origin-height=&quot;1287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVPWAh/btsOtf264vL/rNuKapHPieeilrfxcAcJMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVPWAh/btsOtf264vL/rNuKapHPieeilrfxcAcJMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVPWAh/btsOtf264vL/rNuKapHPieeilrfxcAcJMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVPWAh%2FbtsOtf264vL%2FrNuKapHPieeilrfxcAcJMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1592&quot; height=&quot;720&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;720&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Batch는 청크 단위로 트랜잭션을 관리하기 때문에, 하나의 청크가 실패하면 해당 청크만 롤백되고, Job은 중단되거나 재시작된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;커서 기반 vs 페이징 기반 Reader&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Batch에서 데이터베이스로부터 대량의 데이터를 읽어올 때는 Reader 전략을 신중하게 선택해야 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;대표적으로 사용되는 방식은 &lt;b&gt;커서 기반(Cursor-based)&lt;/b&gt; 과 &lt;b&gt;페이징 기반(Paging-based)&lt;/b&gt; 두 가지다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;커서 기반은 JdbcCursorItemReader와 같은 &lt;b&gt;JDBC 기반 방식&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;페이징 기반은 JpaPagingItemReader와 같은 &lt;b&gt;JPA 기반 방식&lt;/b&gt;으로 구현된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;아래는&amp;nbsp;두&amp;nbsp;방식의&amp;nbsp;차이를&amp;nbsp;정리한&amp;nbsp;표다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0077%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;항목&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.845%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;커서 기반(Cursor-based)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1472%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;페이징 기반(Paging-based)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0077%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;처리 방식&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.845%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;커서를 열고 전체 데이터를 순차적으로 읽음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1472%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;일정 크기로 나누어 반복 쿼리로 읽음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0077%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;쿼리 실행&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.845%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1번 실행 후 커서 유지&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1472%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여러 번 쿼리 실행(page 수만큼)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0077%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;메모리 사용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.845%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;전체 데이터를 가져와 메모리 부담 많음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1472%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;매 페이지마다 작게 나누어 가져와 메모리 부담 적음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0077%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;속도&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.845%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;커넥션 유지로 속도 빠름&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1472%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다수의 쿼리로 인해 상대적으로 느림&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0077%; text-align: center;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #000000; text-align: center; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 데이터 정합성 &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.845%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;처리 중 데이터 변경 없음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1472%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;처리 중 데이터 변경 시 중복/누락 가능성 존재&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0077%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;트랜잭션 처리&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.845%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;트랜잭션 전체 유지&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1472%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;각 페이지 단위로 트랜잭션 분리 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0077%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;커넥션 점유&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.845%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;전체 작업 동안 커넥션 점유&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1472%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;각 페이지마다 커넥션 연결/해제 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0077%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;적합한 상황&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.845%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;정적인 대량 데이터 처리, 빠른 일괄 처리&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1472%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;변동 가능한 데이터, 병렬성/복원력 필요 시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JPA 기반 페이징은 엔티티 관리와 연관 관계 처리 등 ORM의 이점을 활용할 수 있지만, 대량 데이터를 처리할 때는 성능 저하나 메모리 부담이 발생할 수 있다. 반면, 커서 기반은 ResultSet을 열어 한 줄씩 데이터를 스트리밍 방식으로 처리하므로 메모리 사용이 효율적이고 처리 속도도 빠르다. 하지만 커넥션을 오래 점유하게 되므로, 병렬 처리나 재시작이 필요한 경우에는 제약이 크다. 결국, 어떤 방식이 더 낫다고 단정하기보다는 배치 잡의 특성과 시스템 구조에 맞게 전략을 선택하는 것이 중요하다. &lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;청크(Chunk) 기반 처리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;청크(Chunk)&lt;/b&gt; : 일정한 개수로 묶인 데이터 단위.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Spring Batch에서는 chunk(10)처럼 청크 크기를 지정해 설정하며, 각 청크는 다음 순서로 처리된다. &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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; ItemReader가 데이터를 하나씩 읽는다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 지정된 청크 크기만큼 모은다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; ItemProcessor로 데이터를 가공한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; ItemWriter가 가공된 데이터를 한 번에 저장하고 커밋한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; ️ Spring Batch의 Chunk와 Chunk 기반 방식에 대해 설명해주세요.&lt;/b&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; Spring Batch는 기본적으로 Chunk 기반 처리 방식을 사용합니다. Chunk는 데이터를 일정 개수로 묶은 단위이며, 이 단위로 트랜잭션을 커밋합니다. 예를 들어 chunk 크기를 100으로 설정하면, 데이터를 한 건씩 읽고 가공하되 100건이 모일 때까지는 커밋하지 않고, 100건이 쌓인 시점에 한 번에 저장하고 트랜잭션을 커밋하게 됩니다. 이 방식은 매 건마다 커밋하지 않기 때문에 트랜잭션 부하를 줄일 수 있고, 중간에 예외가 발생하더라도 청크 단위로만 롤백되기 때문에 전체 작업을 처음부터 다시 실행할 필요가 없습니다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Chunk 기반 구조는 트랜잭션 범위를 적절히 나눌 수 있고, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;데이터를 일괄 처리하면서도 실패 복구가 가능한 실행 구조를 제공합니다.&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;멱등성 (Idempotency)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;멱등성(Idempotency)&lt;/b&gt; : 같은 작업을 여러 번 수행해도 결과가 변하지 않는 성질.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Batch처럼 &lt;b&gt;대량의 데이터를 청크 단위로 처리하고, 중간 실패 시 재시작이 가능한 구조&lt;/b&gt;에서는 멱등성을 보장하는 것이 데이터 정합성과 안정성을 유지하는 핵심 요소가 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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 data-end=&quot;596&quot; data-start=&quot;570&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DB에 중복된 레코드가 insert됨(DB에 별도의 Unique 제약조건이 없는 경우)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;625&quot; data-start=&quot;597&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이미 처리된 상태의 데이터를 다시 덮어씀&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;664&quot; data-start=&quot;626&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;외부 시스템(API, 메시지 큐 등)에 중복 요청이 발생함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;807&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;실제로는 ItemWriter 단계에서의 멱등성 보장이 가장 중요하다. Writer는 트랜잭션의 커밋 지점이기 때문에, 이 단계에서의 중복/불일치는 곧 데이터 정합성 문제로 직결된다. &lt;span style=&quot;text-align: justify;&quot;&gt;Writer뿐만 아니라 &lt;/span&gt;Reader와 Processor에서도 방어적으로 멱등성을 고려해두면 더 안정적인 설계가 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;807&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;807&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;아래는 SettlementRegisterJobConfig 클래스의 일부이다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-end=&quot;807&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Reader와 Writer 각 단계에서 다음과 같은 방법으로 멱등성이 보장된다.&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 data-end=&quot;807&quot; data-start=&quot;694&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Reader : 이미 settlement가 존재하는 payment는 조회되지 않음&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;807&quot; data-start=&quot;694&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Writer : 별도의 중복 처리 로직 없이도 안전하게 INSERT 가능&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;1222&quot; data-origin-height=&quot;999&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bex5z/btsOvKfOrKN/DOfQkEAcICCxlB7bscZHXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bex5z/btsOvKfOrKN/DOfQkEAcICCxlB7bscZHXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bex5z/btsOvKfOrKN/DOfQkEAcICCxlB7bscZHXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBex5z%2FbtsOvKfOrKN%2FDOfQkEAcICCxlB7bscZHXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1222&quot; height=&quot;999&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;999&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhn7YU/btsOtr3osav/hzglwr30Oga6E9km8cumqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhn7YU/btsOtr3osav/hzglwr30Oga6E9km8cumqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhn7YU/btsOtr3osav/hzglwr30Oga6E9km8cumqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhn7YU%2FbtsOtr3osav%2Fhzglwr30Oga6E9km8cumqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1568&quot; height=&quot;537&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; ️ Spring Batch에서 멱등성은 어떻게 보장했나요?&lt;/b&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; Spring Batch는 청크 단위로 트랜잭션을 커밋하기 때문에 Step 도중 예외가 발생하거나 재시작되는 경우 동일한 데이터가 중복 처리될 수 있는 구조입니다. 그래서 멱등성을 보장하기 위해 Reader 단계에서부터 중복 가능성을 차단하는 조건을 명확히 설정했습니다. 예를 들어 정산 등록 Job에서는 payment 테이블에서 상태가 'DONE'이면서, 이미 settlement에 등록되지 않은 건만 조회하도록 LEFT JOIN + IS NULL 조건을 사용해 처리 대상을 선별했습니다. 정산 완료 Job에서는 settlement 테이블에서 status가 'READY'인 건만 조회해 상태를 'COMPLETED'로 업데이트하 하도록 구성했습니다. 이처럼 Reader에서 중복 대상을 제거하고, Writer는 ID 기반 처리를 통해 이미 처리된 데이터에 대해 다시 커밋되지 않도록 설계했습니다. 덕분에 Job이 재시작되더라도 데이터가 중복 저장되거나 상태가 반복 변경되는 문제 없이, 멱등성이 자연스럽게 보장되는 구조가 됩니다. &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>백엔드</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/395</guid>
      <comments>https://3uomlkh.tistory.com/395#entry395comment</comments>
      <pubDate>Thu, 12 Jun 2025 10:02:40 +0900</pubDate>
    </item>
    <item>
      <title>[STUDY] 동기 vs 비동기 - RabbitMQ로 비동기 이벤트 처리하기</title>
      <link>https://3uomlkh.tistory.com/394</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;  동기 &amp;middot; 비동기와 블로킹&lt;/b&gt;&lt;b&gt;&amp;nbsp;&amp;middot;&amp;nbsp;&lt;/b&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;동기 vs 비동기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;동기(Synchronous) : 호출자와 요청자의 결과 확인이 동시에 일어남&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;비동기(Asynchronous) : 호출자와 요청자의 결과 확인이 동시에 일어나지 않아도 됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;블로킹 vs 논블로킹&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;블로킹(Blocking) : 호출자가 요청에 대한 결과가 나올 때까지 요청자에게 제어권을 돌려주지 않는 것&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;논블로킹(Non-Blocking) : 호출자가 요청에 대한 결과가 나오지 않더라도 요청자에게 제어권을 돌려줌&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;1237&quot; data-origin-height=&quot;915&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dThG61/btsOqK2HBWz/OyCCmEmK5KkHRy4hXn39T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dThG61/btsOqK2HBWz/OyCCmEmK5KkHRy4hXn39T0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dThG61/btsOqK2HBWz/OyCCmEmK5KkHRy4hXn39T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdThG61%2FbtsOqK2HBWz%2FOyCCmEmK5KkHRy4hXn39T0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1237&quot; height=&quot;915&quot; data-origin-width=&quot;1237&quot; data-origin-height=&quot;915&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;동시성 vs 병렬성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;동시성(Concurrency) : 싱글 코어에서 여러 작업을 번갈아 가며 처리하는 방식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;병렬성(Parallelism) : 멀티 코어가 각 작업을 물리적으로 동시에 처리하는 방식&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 예를 들어, 아래 코드는 메시지를 수신할 때 동시에 최대 3개의 메시지를 처리할 수 있도록 &lt;b&gt;concurrency = &quot;3&quot;&lt;/b&gt; 설정을 해두었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이때 Spring은 내부적으로 멀티스레드 풀을 활용해 메시지를 병렬로 처리하려고 시도한다. 만약 이 코드가 단일 CPU 환경에서 실행된다면 실제로는 3개의 스레드가 반갈아 실행되며 각각의 메시지를 처리하게 되는데, 이 때 발생하는 것이 동시성 처리다.&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;1580&quot; data-origin-height=&quot;1272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IhDb1/btsOqqDt08h/9chKYKAi0c7SDHXvf2eZx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IhDb1/btsOqqDt08h/9chKYKAi0c7SDHXvf2eZx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IhDb1/btsOqqDt08h/9chKYKAi0c7SDHXvf2eZx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIhDb1%2FbtsOqqDt08h%2F9chKYKAi0c7SDHXvf2eZx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1580&quot; height=&quot;1272&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;1272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;메시지 큐의 역할과 구조&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;메시지 큐(Message Queue, MQ) : 메시지 지향 미들웨어(Message Oriented Middleware, MOM)의 일종으로, 서로 독립적인 애플리케이션 간에 메시지를 주고받으며 비동기적으로 작업을 처리할 수 있도록 해주는 시스템이다.&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; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;메시징 시스템의 가장 기본적인 구조는 아래와 같이 생산자(Producer)가 메시지를 Message Queue에 넣고, 소비자(Consumer) 가 이를 꺼내 처리하는 구조이다. &lt;span style=&quot;text-align: start;&quot;&gt;Producer&lt;/span&gt;와 &lt;span style=&quot;text-align: start;&quot;&gt;Consumer&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;1386&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8UImn/btsOpWItV8L/ugrwbI1r3ugaGaH88EBzI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8UImn/btsOpWItV8L/ugrwbI1r3ugaGaH88EBzI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8UImn/btsOpWItV8L/ugrwbI1r3ugaGaH88EBzI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8UImn%2FbtsOpWItV8L%2FugrwbI1r3ugaGaH88EBzI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1386&quot; height=&quot;323&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; Producer와 &lt;span style=&quot;text-align: start;&quot;&gt;Consumer&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;가 시간적으로 분리되어 요청-응답 병목 없이 빠른 사용자 응답 가능&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;큐에 메시지를 일시 저장함으로써, 트래픽 폭주나 처리 지연 상황에서도 안정적인 흐름 유지 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;처리 실패 시 메시지를 DLQ 등으로 이동 후 재시도 가능 &amp;rarr; 데이터 유실 없이 안정적인 메시지 처리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;동기 처리의 한계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 아래 코드는 메시지 큐를 도입하기 전, 이벤트 오픈 시 알림을 직접 전송하던 방식이다.&amp;nbsp;openEventsIfDueBefore() 메서드는 DB에서 오픈 대상 이벤트를 조회한 뒤,&amp;nbsp;해당 이벤트에 참여한 사용자 목록을 가져와&amp;nbsp;notificationService.createNotification()을 직접&amp;nbsp;호출해 알림을 전송한다.&amp;nbsp;이때 알림 전송은 동기적으로 처리되며,&amp;nbsp;유저 수가 많아질수록 for문 내부 처리 시간이 길어지고, 전체 트랜잭션이 지연될 가능성이 크다. &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;1280&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vgcZo/btsOrWH1mIK/gMIaLsm7VS2kK1U5OzeOkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vgcZo/btsOrWH1mIK/gMIaLsm7VS2kK1U5OzeOkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vgcZo/btsOrWH1mIK/gMIaLsm7VS2kK1U5OzeOkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvgcZo%2FbtsOrWH1mIK%2FgMIaLsm7VS2kK1U5OzeOkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;667&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;667&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 이벤트 오픈부터 사용자 알림 전송까지 하나의 메서드/트랜잭션 안에서 모두 처리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; for문으로 유저를 하나씩 순회하며 동기적으로 알림 전송 &amp;rarr; 이 모든 작업이 반환될 때까지 대기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 알림 전송 처리량이 늘수록, 전체 오픈 로직 지연이 심해짐 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 실패한 메시지를 따로 저장하거나, 재처리할 수 있는 구조가 없음 &amp;rarr; 유저는 알림을 받지 못하고, 개발자는 실패한 알림이 뭔지 알기 어려움 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;메시지&amp;nbsp;큐&amp;nbsp;도입&amp;nbsp;전후&amp;nbsp;비교&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;MQ를 적용하지 않은 기존 구조의 흐름은 다음과 같다. 알림 전송까지 한 트랜잭션에서 처리되어 응답 지연이 발생할 확률이 높았고, 실패 시 전체 흐름이 중단될 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749109293749&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Scheduler 실행 (주기적)]
       │
       ▼
[DB에서 READY 이벤트 조회]
       │
       ▼
[이벤트 상태 OPEN으로 변경]
       │
       ▼
[유저 조회 &amp;rarr; 알림 직접 전송 (동기)]
       │
       ▼
[완료]&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;MQ 도입 후엔 알림 로직이 메시지 큐를 통해 비동기로 처리되며, Scheduler는 메시지를 발행만 하고, 알림 전송은 Consumer가 병렬로 처리하게 되었다. 구조는 복잡해졌지만, 시스템 확장성과 안정성 측면에선 명확한 개선이 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749109306101&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[이벤트 등록 시]
       │
       ▼
[Redis ZSET에 Event 메시지 저장]
       │
       ▼
[Scheduler 실행 (주기적)]
       │
       ▼
[Redis ZSET에서 오픈 대상 조회]
       │
       ▼
[DB 상태 OPEN으로 변경]
       │
       ▼
[RabbitMQ로 오픈 메시지 발행]
       │
       ▼
[Consumer가 알림 비동기 전송]
       │
       ▼
[완료]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  RabbitMQ 기반 &lt;b&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Exchange 타입&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;   Exchange란? &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RabbitMQ에서 메시지를 수신하고, 적절한 Queue로 라우팅하는 역할을 하는 구성 요소.&lt;br /&gt;Producer가 보낸 메시지는 먼저 Exchange로 도착하고, &lt;br /&gt;Exchange는 메시지의 RoutingKey나 설정된 규칙을 기반으로 어떤 Queue로 메시지를 보낼지 결정한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Direct : 정확히 일치하는 RoutingKey로 메시지를 전달 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Fanout : RoutingKey 없이, 바인딩된 모든 Queue에 메시지를 전파 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Topic : 패턴 기반 RoutingKey 매칭, &lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;라우팅 패턴 간의 와일드카드(*) 혹은 해시(#)가 일치해야 매칭&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Headers : 메시지의 헤더 값을 기반으로 라우팅&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;프로젝트에서는 이벤트 오픈 시, 알림을 설정한 모든 유저에게 알림을 보내야 했기 때문에 Fanout 으로 설정했다.&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;2008&quot; data-origin-height=&quot;1387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjqbaY/btsOp41SdF6/uf1B4sr1gayAVdFM9PkZaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjqbaY/btsOp41SdF6/uf1B4sr1gayAVdFM9PkZaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjqbaY/btsOp41SdF6/uf1B4sr1gayAVdFM9PkZaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjqbaY%2FbtsOp41SdF6%2Fuf1B4sr1gayAVdFM9PkZaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2008&quot; height=&quot;1387&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;1387&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DLQ &lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&amp;middot;&amp;nbsp;&lt;/span&gt; DLX 구조&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2155&quot; data-origin-height=&quot;873&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q00iL/btsOsEmBi3P/hcKdIC62xOB5WrYforQAuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q00iL/btsOsEmBi3P/hcKdIC62xOB5WrYforQAuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q00iL/btsOsEmBi3P/hcKdIC62xOB5WrYforQAuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq00iL%2FbtsOsEmBi3P%2FhcKdIC62xOB5WrYforQAuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2155&quot; height=&quot;873&quot; data-origin-width=&quot;2155&quot; data-origin-height=&quot;873&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; DLQ(Dead Letter Queue) : 메시지 소비에 실패한 경우를 대비해 메시지를 별도로 저장하고 추후 재처리하거나 로그로 확인할 수 있게 해주는 구조&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 실패한 메시지는 DLX &amp;rarr; DLQ로 이동, 이후 수동 재처리 또는 Retry Queue로의 자동 재처리 기반 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;TTL 기반 Retry 처리&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1789&quot; data-origin-height=&quot;1652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/camzO4/btsOrrolyN2/J0vINAVucMmFG1jzil5Eok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/camzO4/btsOrrolyN2/J0vINAVucMmFG1jzil5Eok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/camzO4/btsOrrolyN2/J0vINAVucMmFG1jzil5Eok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcamzO4%2FbtsOrrolyN2%2FJ0vINAVucMmFG1jzil5Eok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1789&quot; height=&quot;1652&quot; data-origin-width=&quot;1789&quot; data-origin-height=&quot;1652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; DLQ에 쌓인 메시지를 자동으로 재시도하기 위해 TTL(Time To Live)과 Retry Queue를 활용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 실패한 메시지는 DLQ에서 다시 Retry Queue로 전송 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 해당 Queue에는 TTL이 설정되어 있어 &lt;b&gt;일정 시간 뒤 원래 Exchange로 재전송&lt;/b&gt; &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>async</category>
      <category>blocking</category>
      <category>DLQ</category>
      <category>DLX</category>
      <category>nonblocking</category>
      <category>RabbitMQ</category>
      <category>Sync</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/394</guid>
      <comments>https://3uomlkh.tistory.com/394#entry394comment</comments>
      <pubDate>Thu, 5 Jun 2025 19:00:26 +0900</pubDate>
    </item>
    <item>
      <title>[STUDY] Spring Security + JWT 로그인/로그아웃 흐름</title>
      <link>https://3uomlkh.tistory.com/393</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이번 주 스터디 주제는 인증/인가다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;최종 프로젝트에서 다른 팀원이 구현한 코드를 분석해보며 인증/인가에 대한 공부도 함께 진행해보려한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Security + JWT를 사용하는 방식은 이전에 강의를 듣고 포스팅 한 적이 있지만,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;우선 인증(Authentication)과 인가(Authorization)에 대해 짚고 가자.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인증(Authentication) &amp;nbsp;: 사용자가 서비스에 가입되어 있는지 확인하는 과정(= 로그인)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인가(Authorization) &amp;nbsp;: 인증된 사용자에 대해 권한을 확인하는 과정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인증/인가가 없다면 식당 예약, 이벤트 참여, 결제와 같은 주요 기능에 누구나 접근할 수 있어 보안상 문제가 발생할 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;또한 관리자만 접근 가능한 기능에 일반 유저가 접근할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; 왜 Spring Security + JWT 인가?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt; Spring Security&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Spring Security&lt;/b&gt; : 스프링 기반 애플리케이션에서 가장 널리 쓰이는 보안 프레임워크.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 공식 홈페이지에는 다음과 같이 설명되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T8WLu/btsOhXmqQ5u/VGmOkvM1KiEDQKQJKRnyy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T8WLu/btsOhXmqQ5u/VGmOkvM1KiEDQKQJKRnyy0/img.png&quot; data-alt=&quot;출처: https://spring.io/projects/spring-security&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T8WLu/btsOhXmqQ5u/VGmOkvM1KiEDQKQJKRnyy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT8WLu%2FbtsOhXmqQ5u%2FVGmOkvM1KiEDQKQJKRnyy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1217&quot; height=&quot;649&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://spring.io/projects/spring-security&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring 기반 애플리케이션의 사실상 표준 보안 프레임워크&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인증(Authentication)과 인가(Authorization) 모두 지원&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;세션 고정, 클릭재킹, CSRF 등 보안 위협에 대한 내장 보호 기능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;필터 기반 구조로 커스터마이징이 용이&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Web MVC와도 쉽게 통합 가능 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;세션 고정&lt;/b&gt; : 공격자가 세션 ID를 미리 정해놓고, 그 세션 ID로 피해자가 로그인하게 만든 뒤, 같은 세션 ID를 이용해 피해자처럼 접근하는 공격. &amp;rarr; 로그인 시 세션 ID를 자동으로 바꿔줌으로써 방어함.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;클락재킹&lt;/b&gt; : 사용자가 알고 누른 게 아닌데, 누르게 만드는 공격. 공격자는 자신의 웹페이지 위에 투명한 iframe을 올려 유저가 어떤 버튼을 누르게 유도함.&amp;nbsp; &amp;rarr; 기본적으로 X-Frame-Options 헤더를 통해 iframe 삽입을 차단함.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;CSRF(Cross Site Request Forgery)&lt;/b&gt; : &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;사용자가 자신의 의지와 상관없이 공격자가 의도한 행위를 특정 웹사이트에 요청하도록 하는 것&lt;/span&gt;. &amp;rarr; CSRF Token을 발급하고, 요청마다 서버에서 토큰 일치 여부 검증.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 Spring Security는 필터 기반 구조라 JWT 기반 인증 플로우를 쉽게 구현할 수 있었고, 인가 처리도 URL/메서드 단위로 유연하게 제어할 수 있어 선택했다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;실제로 최종 프로젝트에서 인가 처리는 &lt;b&gt;@Secured&lt;/b&gt; 어노테이션이 메서드 단위로 적용되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bihTHM/btsOfDpHsRs/BFlRoLH3kWcC0cdUida6jK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bihTHM/btsOfDpHsRs/BFlRoLH3kWcC0cdUida6jK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bihTHM/btsOfDpHsRs/BFlRoLH3kWcC0cdUida6jK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbihTHM%2FbtsOfDpHsRs%2FBFlRoLH3kWcC0cdUida6jK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;861&quot; height=&quot;281&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;JWT(Json Web Token)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;JWT(Json Web Token)&lt;/b&gt; : 토큰 자체에 정보가 포함되어 있는 클레임 기반 토큰으로, 통신 정보를 JSON 형식을 사용하여 안전하게 전송하기 위해 사용됨.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;JWT의 구조&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JWT의 구조는 다음과 같이 &lt;b&gt;헤더(Header), 페이로드(Payload), 서명(Signature)&lt;/b&gt;으로 나뉜다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxfidC/btsOgN5NLk8/4huxhTgihKuZcOYo9Nj0j0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxfidC/btsOgN5NLk8/4huxhTgihKuZcOYo9Nj0j0/img.png&quot; data-alt=&quot;출처: https://jwt.io/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxfidC/btsOgN5NLk8/4huxhTgihKuZcOYo9Nj0j0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxfidC%2FbtsOgN5NLk8%2F4huxhTgihKuZcOYo9Nj0j0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;892&quot; height=&quot;762&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://jwt.io/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 헤더(Header) : 토큰 타입, 서명 알고리즘&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;페이로드(Payload) : 사용자 정보(Claims), 만료 시간 등&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;서명(Signature) : 토큰 위조 방지용 서명&lt;b&gt;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;JWT의 특징&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;서버가 상태를 저장하지 않음(Stateless) &lt;span style=&quot;background-color: #fafafa; text-align: left;&quot;&gt;&amp;rarr;&lt;/span&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;background-color: #ffffff; color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 세션 기반 인증에 비해서 사용자 정보를 조회하기 위한 추가적인 작업이 필요하지 않음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;디코딩이 쉬워 민감한 정보를 담는 것에 유의해야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;JWT 탈취 시 위험 &lt;span style=&quot;background-color: #fafafa; text-align: left;&quot;&gt;&amp;rarr;&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt; &lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 최종 프로젝트에서는 Stateless 구조의 특성과, 수평 확장에 유리한 구조, 웹/모바일 등 다양한 클라이언트 환경과의 연동 편의성을 고려해 Spring Security와 연계한 JWT 기반 인증 방식을 선택했다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;세션 방식과 비교&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 96.6272%; height: 117px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 19.3798%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;항목&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.4175%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;세션 기반 인증&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40.8305%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JWT 기반 인증&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 19.3798%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;상태 관리&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.4175%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Stateful(서버가 세션 저장)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40.8305%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Stateless(서버가 상태 저장X)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 19.3798%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인증 정보 저장 위치&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.4175%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;서버 세션(JSESSIONID)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40.8305%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;클라이언트가 토큰 저장&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 19.3798%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;서버 확장성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.4175%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;세션 공유 필요(Sticky Session, Redis 등)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40.8305%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;수평 확장에 유리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 19.3798%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;요청 인증 방식&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.4175%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;쿠키에 &lt;span style=&quot;background-color: #f9f9f9; text-align: start;&quot;&gt;JSESSIONID 자동 포함&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40.8305%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Authorization 헤더에 Bearer 토큰 명시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 19.3798%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;로그아웃 처리&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.4175%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;세션 무효화&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40.8305%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;블랙리스트 처리, Refresh Token 삭제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 19.3798%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;클라이언트 호환성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.4175%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;브라우저 중심&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40.8305%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;웹/모바일 등 다양한 클라이언트에 적합&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1. 로그인 요청 수신&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Access Token : 본문 응답으로 전달&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Refresh Token : CookieUtil을 통해 응답 쿠키에 저장&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;1397&quot; data-origin-height=&quot;505&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyBdb2/btsOg5evyhk/j4GDSKqxYuBnKp99n4k92k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyBdb2/btsOg5evyhk/j4GDSKqxYuBnKp99n4k92k/img.png&quot; data-alt=&quot;AuthController.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyBdb2/btsOg5evyhk/j4GDSKqxYuBnKp99n4k92k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyBdb2%2FbtsOg5evyhk%2Fj4GDSKqxYuBnKp99n4k92k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1397&quot; height=&quot;505&quot; data-origin-width=&quot;1397&quot; data-origin-height=&quot;505&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AuthController.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;2. 사용자 조회 및 탈퇴 여부 확인, 비밀번호 검증&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;먼저 이메일로 사용자를 조회하고, 이미 탈퇴 처리된 사용자라면 예외를 발생시킨다. 즉, &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;deletedAt&lt;/b&gt; 필드가 &lt;b&gt;null&lt;/b&gt;이 아니라면 이미 탈퇴한 사용자로 간주한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;요청에서 받은 비밀번호와 DB에 저장된 비밀번호를 비교해 검증한다. 비밀번호 검증은 &lt;b&gt;Spring Security&lt;/b&gt;에서 제공하는&lt;b&gt;BCryptPasswordEncoder&lt;/b&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;1298&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OGbgD/btsOf3VZubb/bkp3exrdghad6zcPqsrcMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OGbgD/btsOf3VZubb/bkp3exrdghad6zcPqsrcMK/img.png&quot; data-alt=&quot;AuthService.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OGbgD/btsOf3VZubb/bkp3exrdghad6zcPqsrcMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOGbgD%2FbtsOf3VZubb%2Fbkp3exrdghad6zcPqsrcMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1298&quot; height=&quot;638&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AuthService.java&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;3. Access Token &amp;amp; Refresh Token 생성&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인증이 완료된 유저 정보를 기반으로 JWT 토큰을 발급한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Access Token은 사용자 인증에 사용되고, Refresh Token은 재발급과 로그아웃 처리에 사용된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkR2se/btsOhJPueYa/xYCYw1miTSTLNpeUDYJRbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkR2se/btsOhJPueYa/xYCYw1miTSTLNpeUDYJRbk/img.png&quot; data-alt=&quot;AuthService.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkR2se/btsOhJPueYa/xYCYw1miTSTLNpeUDYJRbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkR2se%2FbtsOhJPueYa%2FxYCYw1miTSTLNpeUDYJRbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;759&quot; height=&quot;334&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AuthService.java&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-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; createAccessToken(user)&lt;/span&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 data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;sub, userId, role, nickname, email, iat, exp 등을 claim에 포함한 JWT를 생성&lt;/span&gt;&lt;/li&gt;
&lt;li data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;JwtUtil.createToken()&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 메서드를 내부적으로 사용&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcAgk7/btsOg6RY6jb/sKcn24r2JzMd0cHsBHir1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcAgk7/btsOg6RY6jb/sKcn24r2JzMd0cHsBHir1k/img.png&quot; data-alt=&quot;TokenService.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcAgk7/btsOg6RY6jb/sKcn24r2JzMd0cHsBHir1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcAgk7%2FbtsOg6RY6jb%2FsKcn24r2JzMd0cHsBHir1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;519&quot; height=&quot;233&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TokenService.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1621&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VKRNe/btsOfQvHQZM/Km4Y5Hj3KIf6ZR4JAZRSg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VKRNe/btsOfQvHQZM/Km4Y5Hj3KIf6ZR4JAZRSg0/img.png&quot; data-alt=&quot;JwtUtil.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VKRNe/btsOfQvHQZM/Km4Y5Hj3KIf6ZR4JAZRSg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVKRNe%2FbtsOfQvHQZM%2FKm4Y5Hj3KIf6ZR4JAZRSg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1621&quot; height=&quot;734&quot; data-origin-width=&quot;1621&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JwtUtil.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;  createRefreshToken(user)&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Refresh Token도 JWT 형식으로 생성되며, userId를 값으로 Redis에 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;클라이언트엔 쿠키로 전달&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1344&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cmcdh/btsOh975zxR/RrN1nAcTeyV9xQnu8fpuUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cmcdh/btsOh975zxR/RrN1nAcTeyV9xQnu8fpuUk/img.png&quot; data-alt=&quot;TokenService.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cmcdh/btsOh975zxR/RrN1nAcTeyV9xQnu8fpuUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCmcdh%2FbtsOh975zxR%2FRrN1nAcTeyV9xQnu8fpuUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1344&quot; height=&quot;916&quot; data-origin-width=&quot;1344&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TokenService.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Refresh Token을 사용한 이유&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Access Token만 사용한다면?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1. 사용자 경험 저하 가능성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Access Token은 Stateless 인증 수단으로, 서버가 별도로 저장하지 않아 관리가 어렵다. 따라서 토큰이 탈취될 경우의 피해를 줄이기 위해, 일반적으로 유효기간을 짧게 설정한다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;338&quot; data-start=&quot;294&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;유효기간이 짧기 때문에 사용자는 빈번하게 재로그인을 요구받게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;385&quot; data-start=&quot;339&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Access Token은 발급 이후 서버가 따로 보관하지 않기 때문에 그 토큰이 누가 들고 있든, 만료 전까지는 무조건 유효하다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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 data-end=&quot;621&quot; data-start=&quot;596&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;서버는 이를 감지하거나 차단할 방법이 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;654&quot; data-start=&quot;622&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;유저 본인이 탈취를 인지하지 않는 이상 방어할 수 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;702&quot; data-start=&quot;655&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Refresh Token이 필요한 이유&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1. 인증 상태를 유지하면서 UX를 보장&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 data-pm-slice=&quot;1 1 [&amp;quot;list&amp;quot;,{&amp;quot;spread&amp;quot;:false,&amp;quot;start&amp;quot;:4340,&amp;quot;end&amp;quot;:4429},&amp;quot;regular_list_item&amp;quot;,{&amp;quot;start&amp;quot;:4340,&amp;quot;end&amp;quot;:4393}]&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Access Token이 만료되더라도 Refresh Token으로 새로운 토큰을 재발급할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;2. 탈취 대응 및 서버 측 제어 가능&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 data-pm-slice=&quot;1 1 [&amp;quot;list&amp;quot;,{&amp;quot;spread&amp;quot;:false,&amp;quot;start&amp;quot;:4457,&amp;quot;end&amp;quot;:4553},&amp;quot;regular_list_item&amp;quot;,{&amp;quot;start&amp;quot;:4457,&amp;quot;end&amp;quot;:4495}]&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Refresh Token은 서버에 저장되어 관리되기 때문에 로그아웃 시 삭제하거나 TTL 만료 설정이 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-pm-slice=&quot;1 1 [&amp;quot;list&amp;quot;,{&amp;quot;spread&amp;quot;:false,&amp;quot;start&amp;quot;:4457,&amp;quot;end&amp;quot;:4553},&amp;quot;regular_list_item&amp;quot;,{&amp;quot;start&amp;quot;:4457,&amp;quot;end&amp;quot;:4495}]&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;탈취 의심 시 즉시 무효화할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-pm-slice=&quot;1 1 [&amp;quot;list&amp;quot;,{&amp;quot;spread&amp;quot;:false,&amp;quot;start&amp;quot;:4457,&amp;quot;end&amp;quot;:4553},&amp;quot;regular_list_item&amp;quot;,{&amp;quot;start&amp;quot;:4457,&amp;quot;end&amp;quot;:4495}]&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Refresh Token 자체는 HttpOnly + Secure 쿠키로 보호되며, 클라이언트에서 직접 접근할 수 없다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;실제 코드에서도 Refresh Token은 HttpOnly + Secure 옵션을 명시했다. 이렇게 설정함으로써 자바스크립트 접근 차단과&amp;nbsp; HTTPS 연결 제한을 동시에 적용하고 있다.&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;1713&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bN0jiZ/btsOfYNX8I3/gEYCXCLkYKxEp8xCoKWQb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bN0jiZ/btsOfYNX8I3/gEYCXCLkYKxEp8xCoKWQb1/img.png&quot; data-alt=&quot;CookieUtil.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bN0jiZ/btsOfYNX8I3/gEYCXCLkYKxEp8xCoKWQb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbN0jiZ%2FbtsOfYNX8I3%2FgEYCXCLkYKxEp8xCoKWQb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1713&quot; height=&quot;407&quot; data-origin-width=&quot;1713&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CookieUtil.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1. 로그아웃 요청 수신&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;클라이언트는 로그아웃 시 Authorization 헤더에 Access Token을 담고, Refresh Token 쿠키도 함께 전송한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1559&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b54LnC/btsOf0LKo66/tEeBtLgkPVap2lspm1BAt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b54LnC/btsOf0LKo66/tEeBtLgkPVap2lspm1BAt1/img.png&quot; data-alt=&quot;AuthController.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b54LnC/btsOf0LKo66/tEeBtLgkPVap2lspm1BAt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb54LnC%2FbtsOf0LKo66%2FtEeBtLgkPVap2lspm1BAt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1559&quot; height=&quot;596&quot; data-origin-width=&quot;1559&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AuthController.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;2. &lt;span style=&quot;text-align: start;&quot;&gt;Refresh Token 제거 및 블랙리스트 등록&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt; 클라이언트가 보낸 &lt;span style=&quot;text-align: start;&quot;&gt;Refresh Token을 Redis에서 제거하고, &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Access Token은 블랙리스트에 등록해 더 이상 사용하지 못하도록 처리한다.&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;1366&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXEgx4/btsOicw0fkn/3CaUHvq6j4h3wuskiMDI20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXEgx4/btsOicw0fkn/3CaUHvq6j4h3wuskiMDI20/img.png&quot; data-alt=&quot;AuthService.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXEgx4/btsOicw0fkn/3CaUHvq6j4h3wuskiMDI20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXEgx4%2FbtsOicw0fkn%2F3CaUHvq6j4h3wuskiMDI20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1366&quot; height=&quot;307&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;307&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AuthService.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Refresh Token은 Redis에 refreshToken:{token} 형태로 저장돼 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;로그아웃 시 이 값을 삭제함으로써 이후 해당 토큰을 사용한 Access Token 재발급 요청을 차단할 수 있다.&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;1911&quot; data-origin-height=&quot;595&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VZRvB/btsOgMfcxRh/tDbLZEHKKxSjYklE2g6d5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VZRvB/btsOgMfcxRh/tDbLZEHKKxSjYklE2g6d5k/img.png&quot; data-alt=&quot;TokenService.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VZRvB/btsOgMfcxRh/tDbLZEHKKxSjYklE2g6d5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVZRvB%2FbtsOgMfcxRh%2FtDbLZEHKKxSjYklE2g6d5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1911&quot; height=&quot;595&quot; data-origin-width=&quot;1911&quot; data-origin-height=&quot;595&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TokenService.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Access Token은 서버에서 따로 관리하지 않기 때문에 한 번 발급되면 만료 전까지는 항상 유효하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이로 인해 사용자가 로그아웃하거나 탈퇴해도 누구든 기존의 Access Token을 계속 사용할 수 있다는 보안 취약점이 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;  블랙리스트(Blacklist)란?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;블랙리스트란 더 이상 유효하지 않다고 판단되는 JWT Access Token을 별도의 저장소에 보관해두고, 이후 해당 토큰이 사용된 요청을 서버에서 차단하는 전략이다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;다음과 같이 토큰을 Redis에 블랙리스트로 등록하고,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;남은 유효 시간만큼 TTL을 설정해&amp;nbsp;&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-origin-width=&quot;1555&quot; data-origin-height=&quot;639&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dL6841/btsOhXtkHpa/H08eLuuz7a6jOMlHnHjdu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dL6841/btsOhXtkHpa/H08eLuuz7a6jOMlHnHjdu0/img.png&quot; data-alt=&quot;TokenService.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dL6841/btsOhXtkHpa/H08eLuuz7a6jOMlHnHjdu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdL6841%2FbtsOhXtkHpa%2FH08eLuuz7a6jOMlHnHjdu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1555&quot; height=&quot;639&quot; data-origin-width=&quot;1555&quot; data-origin-height=&quot;639&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TokenService.java&lt;/figcaption&gt;
&lt;/figure&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 data-end=&quot;1342&quot; data-start=&quot;1314&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Key&lt;/b&gt;: blacklistToken:{JWT}&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1393&quot; data-start=&quot;1343&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Value&lt;/b&gt;: 이유 + 사용자 정보 (ex. logout:userId=123)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1419&quot; data-start=&quot;1394&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;TTL&lt;/b&gt;: 토큰 만료 시점까지만 유지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Access Token이 블랙리스트에 등록되면, JwtAuthenticationFilter 내부에서 아래와 같은 순서로 토큰 유효성을 검사한다.&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;2785&quot; data-origin-height=&quot;1299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjGOuR/btsOi8g6oHC/grkjwj7kVWx7U41axkUD8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjGOuR/btsOi8g6oHC/grkjwj7kVWx7U41axkUD8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjGOuR/btsOi8g6oHC/grkjwj7kVWx7U41axkUD8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjGOuR%2FbtsOi8g6oHC%2Fgrkjwj7kVWx7U41axkUD8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2785&quot; height=&quot;1299&quot; data-origin-width=&quot;2785&quot; data-origin-height=&quot;1299&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;346&quot; data-start=&quot;312&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Authorization 헤더에서 JWT 추출&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;386&quot; data-start=&quot;347&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Redis에 해당 토큰이 블랙리스트로 등록되어 있는지 조회&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;420&quot; data-start=&quot;387&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;등록된 토큰일 경우 &amp;rarr; 인증 중단 + 401 응답 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;&amp;nbsp;&lt;/p&gt;</description>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/393</guid>
      <comments>https://3uomlkh.tistory.com/393#entry393comment</comments>
      <pubDate>Thu, 29 May 2025 18:57:25 +0900</pubDate>
    </item>
    <item>
      <title>[STUDY] 동시성 제어 개념과 방법 알아보기 </title>
      <link>https://3uomlkh.tistory.com/392</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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;최종 프로젝트가 끝난지 벌써 2주가 됐다...!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 좀 더 다양한 동시성 제어 방법에 대해 공부하지 않은 점&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DB 락 구현이 잘못된 점&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서 팀원들과 매주 모여 자신이 공부한 내용을 발표하고 의견을 나눠보기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;주제는 최종 프로젝트의 핵심적인 내용 위주로 진행될 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;첫 번째 주제는 &lt;b&gt;&quot;동시성 제어&quot;&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;내가 맡은 부분이긴하지만 어렵고 헷갈리는 부분이 많아 쉽지 않은 주제인 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 동시성(Cocurrency) 제어란?&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt; 둘 이상의 실행 흐름(스레드 또는 트랜잭션)이 동시에 동일한 자원에 접근할 때 발생할 수 있는 데이터 충돌이나 정합성 오류를 방지하기 위한 제어 기법.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 선착순 100명 이벤트에 동시에 200명이 신청할 경우, 100명을 초과해 참여가 완료됨.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 재고가 100개 남았을 때 동시에 100건의 주문이 들어올 경우, 재고 수량과 실제 판매량이 맞지 않음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;zwj; 동시성 제어를 하지 않는다면...&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;경쟁 상태(Race Condition)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;여러 프로세스 혹은 스레드에서 자원에 접근하는 순서에 따라 데이터 상태나 시스템의 동작 결과가 달라지는 현상.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;엄마가 냉장고를 열어 우유가 없는 것을 확인한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;엄마가 우유를 사러 마트에 간다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;엄마가 우유를 사서 돌아오는 길에 아빠가 냉장고에 우유가 없는 것을 확인한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아빠가 우유를 사러 마트에 간다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;에상한 결과는 우유가 1개가 되는 것이었지만, 실제로는 2개가 되어버렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 재고 수량이 음수가 되는 현상 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이벤트 정원을 초과해서 성공 처리되는 현상&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 포인트 중복 적립 또는 누락&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; ✅ 임계 영역&lt;/b&gt; : 공유 자원에 접근할 수 있고 접근 순서에 따라 결과가 달라지는 코드 영역.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;(위 예시에서는 냉장고에 우유 유무를 판단하고 우유를 추가하는 부분)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;- 임계 영역에서 경쟁 상태가 발생하는 것을 방지하려면 여러 프로세스가 공유 자원에 접근해도 데이터의 일관성이 유지되도록 &lt;b&gt;프로세스 동기화&lt;/b&gt;를 해야한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;- 임계 영역에 대한 접근을 동기화하는 것이 동시성 제어의 핵심이다.&amp;nbsp;&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;동기(Blocking) : 호출자가 요청에 대한 결과가 나올 때까지 요청자에게 제어권을 돌려주지 않는 것.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 비동기(non-blocking) : 호출자가 요청에 대한 결과가 나오지 않더라도 요청자에게 제어권을 돌려줌.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;싱크(Sync) : 호출자와 요청자의 결과 확인이 동시에 일어남.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;어싱크(Async) : 호출자와 요청자의 결과 확인이 동시에 일어나지 않아도 됨.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;교착 상태(Deadlock)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;2개 이상의 프로세스가 각각 자원을 가지고 있으면서 서로의 자원을 요구하며 기다리는 상태.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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 data-end=&quot;1474&quot; data-start=&quot;1437&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;철학자들은 포크(자원)를 양손에 들어야 식사를 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1512&quot; data-start=&quot;1475&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;모두 동시에 왼쪽 포크를 들고, 오른쪽 포크가 놓이길 기다리면?&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1553&quot; data-start=&quot;1513&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;상호배제(mutual exclusion) : 하나의 공유 자원에 하나의 프로세스만 접근할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;점유와 대기(hold and wait) : 프로세스가 최소 하나의 자원을 점유하고 있는 상태에서 추가로 다른 프로세스에서 사용 중인 자원을 점유하기 위해 대기한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;비선점(non-preemption) : 다른 프로세스에 할당된 자원을 뺏을 수 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;환형 대기(circular wait) : 프로세스가 자신의 자원을 점유하면서 앞이나 뒤에 있는 프로세스의 자원을 요구한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 동시성 제어 방식은 단일 서버 내 스레드 수준의 처리부터, 데이터베이스 트랜잭션, 분산 환경까지 다양한 계층에서 이뤄질 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; synchronized&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;자바에서 제공하는 가장 기본적인 동시성 제어 키워드.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; 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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단일 JVM 내에서 여러 스레드가 동시에 공유 자원에 접근할 때 충돌을 방지하기 위해 사용된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단일 서버, 단일 JVM 내부의 멀티스레드 환경에서만 효과있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;락 범위가 커지면 성능 병목이 발생할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다음과 같이 간단한 재고 감소 로직으로 테스트 해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYHxOQ/btsN6742yAX/8MUvktSHXgZl71mtevoiXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYHxOQ/btsN6742yAX/8MUvktSHXgZl71mtevoiXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYHxOQ/btsN6742yAX/8MUvktSHXgZl71mtevoiXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYHxOQ%2FbtsN6742yAX%2F8MUvktSHXgZl71mtevoiXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;607&quot; height=&quot;201&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;201&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;동시에 100개 요청 테스트를 해보니 재고가 10개밖에 감소되지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clAG4p/btsN7VW1TaJ/Rpto52Hs9LuaaInFk9yhr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clAG4p/btsN7VW1TaJ/Rpto52Hs9LuaaInFk9yhr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clAG4p/btsN7VW1TaJ/Rpto52Hs9LuaaInFk9yhr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclAG4p%2FbtsN7VW1TaJ%2FRpto52Hs9LuaaInFk9yhr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1246&quot; height=&quot;269&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;269&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이번에는 synchronized 키워드를 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVscor/btsN8BQ7jwW/tKhvakk8LJntA7r23BTnsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVscor/btsN8BQ7jwW/tKhvakk8LJntA7r23BTnsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVscor/btsN8BQ7jwW/tKhvakk8LJntA7r23BTnsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVscor%2FbtsN8BQ7jwW%2FtKhvakk8LJntA7r23BTnsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;146&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;174&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;정상적으로 100개가 감소되었다.&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;1234&quot; data-origin-height=&quot;141&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUWvkh/btsN8tNFf1L/uJFWx43WSkBkeaExFJMUK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUWvkh/btsN8tNFf1L/uJFWx43WSkBkeaExFJMUK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUWvkh/btsN8tNFf1L/uJFWx43WSkBkeaExFJMUK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUWvkh%2FbtsN8tNFf1L%2FuJFWx43WSkBkeaExFJMUK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1234&quot; height=&quot;141&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;141&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;ReentrantLock&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt; synchronized보다 더 세밀한 제어가 가능한 명시적 락 구현체.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자바의 java.util.concurrent.locks 패키지에 포함되어 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;임계 영역을 직접 lock/unlock 할 수 있는 구조를 제공한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;락 획득 방식, 공정성, 대기 시간 설정 등에서 synchronized보다 유연하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 낙관적 락 vs 비관적 락&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;중돌이 자주 발생하는 상황인가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;읽기와 수정하기의 비율은 어디에 가까운가?&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;낙관적 락 (Optimistic Lock)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;충돌이 없을 것이라고 낙관적으로 예상하는 방법.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실제로 Lock을 이용하지 않고 Version을 이용해 정합성을 맞춘다. ( JPA에서는&amp;nbsp; &lt;b&gt;@Version&lt;/b&gt; 어노테이션 사용 )&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;어플리케이션 락&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데드락의 가능성이 적고 성능의 이점이 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;충돌이 발생하면 오버헤드가 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;비관적 락(Pessimistic Lock)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;충돌을 예상하고 미리 락을 거는 방법.&lt;br /&gt;&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실제로 데이터에 락을 걸어 정합성을 맞춘다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터베이스 락&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;충돌에 대한 오버헤드가 줄어들고 무결성을 지키기 용이하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;충돌이 없으면 오버헤드가 발생한다. &amp;rarr;&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최종 프로젝트에서 선착순 이벤트 역시 한 번에 수많은 요청이 몰릴 것이 분명한 구조였기 때문에,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;충돌이 실제로 발생할 가능성은 높다고 판단했다. 따라서 다음과 같이 비관적 락을 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1l5vp/btsN9eIq00p/Q5WUJ7qAQPwKs7rmsnzJR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1l5vp/btsN9eIq00p/Q5WUJ7qAQPwKs7rmsnzJR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1l5vp/btsN9eIq00p/Q5WUJ7qAQPwKs7rmsnzJR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1l5vp%2FbtsN9eIq00p%2FQ5WUJ7qAQPwKs7rmsnzJR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;586&quot; height=&quot;108&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OS778/btsN77wV6X0/QxkwpuBuTARyGQD3540jkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OS778/btsN77wV6X0/QxkwpuBuTARyGQD3540jkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OS778/btsN77wV6X0/QxkwpuBuTARyGQD3540jkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOS778%2FbtsN77wV6X0%2FQxkwpuBuTARyGQD3540jkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;698&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;findByIdForUpdate()는 내부적으로 &lt;b&gt;SELECT ... FOR UPDATE&lt;/b&gt;를 수행하여 이벤트 row를 트랜잭션 동안 잠금 상태로 유지한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이벤트 상태 확인, 중복 참여 검사, 좌석 감소, 참여 저장까지의 일련의 작업은 모두 하나의 트랜잭션 안에서 수행되며, 동시 접근으로부터 보호된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Redis 분산 락&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;(내용 추가 예정)&lt;/span&gt;&lt;/p&gt;</description>
      <category>동시성제어</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/392</guid>
      <comments>https://3uomlkh.tistory.com/392#entry392comment</comments>
      <pubDate>Thu, 22 May 2025 17:00:09 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Redis ZSET + RabbitMQ 적용해 이벤트 오픈 알림 구현하기</title>
      <link>https://3uomlkh.tistory.com/391</link>
      <description>&lt;h1&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존 코드의 문제점&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존 이벤트 API는 오픈 알림이 구현되지 않았고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단순 DB 기반 스케줄러로 1분마다 오픈 시간을 체크해 상태를 변경하는 구조였다.&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;1380&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsx7Ru/btsNw32tyui/xKfHC6l3k2O8O4rjDK5Bpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsx7Ru/btsNw32tyui/xKfHC6l3k2O8O4rjDK5Bpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsx7Ru/btsNw32tyui/xKfHC6l3k2O8O4rjDK5Bpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbsx7Ru%2FbtsNw32tyui%2FxKfHC6l3k2O8O4rjDK5Bpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1380&quot; height=&quot;774&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1092&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/drG3nb/btsNwTsx16f/gTfYZS45Vq9MznHntPf1k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/drG3nb/btsNwTsx16f/gTfYZS45Vq9MznHntPf1k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/drG3nb/btsNwTsx16f/gTfYZS45Vq9MznHntPf1k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdrG3nb%2FbtsNwTsx16f%2FgTfYZS45Vq9MznHntPf1k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;1092&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1092&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;스케줄러는 매 분마다 WHERE open_at &amp;lt;= NOW() 조건으로 &lt;b&gt;모든 이벤트를 조회&lt;/b&gt;해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 기존 코드는 매 분마다 위와 같은 조건으로 풀스캔하고 있다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 인덱스를 적용하더라도 유저 수와 이벤트 수가 많아질수록 쿼리 부하가 커지고, 인덱싱만으로는 처리 한계가 생긴다. &lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;두 서비스가 서로의 생명주기와 내부 구조에 영향을 받을 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;직접적인 연결은 유지보수를 어렵게 하고, 장애 전파 가능성을 높인다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MQ를 사용하면 이벤트 서비스는 오픈 시 메시지만 발행하고, 알림 도메인은 메시지를 수신해 독립적으로 처리함으로써 구조적으로 느슨한 연결과 안정적인 전송이 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기능 구현&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이벤트 생성 시 Redis ZSET 등록&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존 &lt;b&gt;createEvent()&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;1672&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq1p4o/btsNwWCyMDv/j8KK03LJB87wNapvWSfUlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq1p4o/btsNwWCyMDv/j8KK03LJB87wNapvWSfUlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq1p4o/btsNwWCyMDv/j8KK03LJB87wNapvWSfUlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq1p4o%2FbtsNwWCyMDv%2Fj8KK03LJB87wNapvWSfUlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;770&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;770&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;openEpoch&lt;/b&gt; : LocalDateTime 타입인 openAt 값을 &lt;b&gt;UTC 기준 초 단위 숫자(Epoch Time)&lt;/b&gt; 로 변환한 것. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; Redis ZSET에서는 score(정렬 기준)가 double 숫자여야 하고, 시간 비교를 위해 초 단위 숫자가 가장 직관적이기 때문에 이와 같은 방식으로 변환.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ZSet에 event:open 을 key로, 이벤트의 ID를 value로 넣고 이벤트의 오픈 시간을 정렬 기준으로 등록했다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이벤트 오픈 시간 확인 후 상태 변경 - openEventsIfDue()&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존 코드에서는 스케줄러를 통해 &lt;b&gt;openEventsIfDue()&lt;/b&gt;가 실행되면 다음과 같은 흐름으로 진행됐다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;[1분마다 실행되는 스케줄러]
      &amp;darr;
[DB 조회]
  - 이벤트 테이블에서 
    상태가 READY이고 open_at &amp;le; 현재시간인 이벤트 전체 조회
      &amp;darr;
[DB 상태 변경]
  - 해당 이벤트의 상태를 OPENED로 변경
      &amp;darr;
[유저 알림 생성]
  - 참여 유저 목록을 조회하여
  - 알림을 직접 동기적으로 생성 (for-loop 돌면서 NotificationService 호출)
      &amp;darr;
[끝]
  - 유저에게는 알림이 전달되지만,
  - 동기 처리로 트랜잭션 지연 및 서비스 결합 발생 가능&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;개선된 &lt;b&gt;openEventsIfDue()&lt;/b&gt;의 처리 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;[1분마다 실행되는 스케줄러]
      &amp;darr;
[Redis ZSET 조회]
  - event:open ZSET에서
    score(=오픈 시간) &amp;le; now 인 ID들만 조회
      &amp;darr;
[각 ID별로 처리 반복]
      &amp;darr;
[1. 이벤트 엔티티 조회 및 상태 확인]
      &amp;darr;
[2. 상태가 READY면 OPENED로 변경]
      &amp;darr;
[3. MQ에 EventOpenMessage 발행]
      &amp;darr;
[4. Redis ZSET에서 해당 ID 제거]
      &amp;darr;
[끝]
  - 실제 알림은 Consumer가 비동기로 처리&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;리팩토링한 openEventsIfDue() 인데, 예약된 이벤트의 오픈 시간이 도달하면 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 상태를 변경&lt;/b&gt;하고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 메시지를 MQ로 발행해 후속 처리를 비동기 전파&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;1426&quot; data-origin-height=&quot;1830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAGI97/btsNw0dSVPs/XZN4QKCqMAVYVS1ByTWbkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAGI97/btsNw0dSVPs/XZN4QKCqMAVYVS1ByTWbkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAGI97/btsNw0dSVPs/XZN4QKCqMAVYVS1ByTWbkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAGI97%2FbtsNw0dSVPs%2FXZN4QKCqMAVYVS1ByTWbkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1426&quot; height=&quot;1830&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;1830&quot;/&gt;&lt;/span&gt;&lt;/figure&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;rangeByScore(0, now)로 오픈 시간이 도달한 이벤트 ID만 선별 조회&amp;rarr; DB 쿼리 없이 오픈 대기 이벤트만 O(logN)으로 조회 가능 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(= score 값이 0 이상 now 이하인 요소들의 value들(ID들)만 조회)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이벤트가 아직 READY 상태인지 확인 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; READY 가 아닐 경우 건너뛰고, READY 상태라면 OPENED 로 상태 변경&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;오픈된 이벤트 정보를 MQ로 발행 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; 알림/로그/통계 등은 직접 처리하지 않고, Consumer가 비동기로 처리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;성공적으로 처리된 이벤트는 ZSET에서 제거&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EventOpenProducer&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위에서 메세지 발행을 하기 전 EventOpenProducer 클래스를 만들어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;해당 클래는 이벤트가 오픈되는 시점에 해당 이벤트 정보를 RabbitMQ에 메시지로 발행하는 역할을 한다.&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;1662&quot; data-origin-height=&quot;899&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1hdOr/btsNxCXBiM3/ZkFzFu8DOZD0HNFeCvxlPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1hdOr/btsNxCXBiM3/ZkFzFu8DOZD0HNFeCvxlPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1hdOr/btsNxCXBiM3/ZkFzFu8DOZD0HNFeCvxlPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1hdOr%2FbtsNxCXBiM3%2FZkFzFu8DOZD0HNFeCvxlPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1662&quot; height=&quot;899&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;899&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  이벤트 오픈 메시지를 fanout exchange로 발행한 이유&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;현재 이벤트가 등록되고, 이벤트가 열리면 발행은 모든 유저에게 하되, Customer 쪽에서 알림 수신 설정을 한 유저에게만 알림을 보내도록 했다. 사용자 설정과 같은 동적인 조건에 따라 메시지를 처리해야 하는 경우, Consumer 측에서 이러한 로직을 구현하는 것이 유연성과 확장성 측면에서 유리할 수 있다고 한다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  왜 routingKey는 빈 문자열(&quot;&quot;)로 설정하는가?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;FanoutExchange는 메시지를 받으면 연결된 모든 큐에 브로드캐스트하기 때문에 라우팅 키가 필요하지 않다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이벤트 오픈 시간에 맞춰 테스트 한 후 로그를 보니 이벤트 오픈 메세지가 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333; text-align: start;&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-origin-width=&quot;2048&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcne8g/btsNxvKW6t9/2GGJl3kk4ukjkG5tmLONB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcne8g/btsNxvKW6t9/2GGJl3kk4ukjkG5tmLONB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcne8g/btsNxvKW6t9/2GGJl3kk4ukjkG5tmLONB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcne8g%2FbtsNxvKW6t9%2F2GGJl3kk4ukjkG5tmLONB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;79&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;79&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EventOpenConsumer&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이제 발행을 했으면 메세지를 수신해야 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EventOpenConsumer 클래스는 RabbitMQ를 통해 전송된 이벤트 오픈 메시지를 수신하고, 유저에게 알림을 생성하는 클래스다.&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;1582&quot; data-origin-height=&quot;1107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LbB26/btsNvdSu2NK/XdtsiZG8JN4ZnkKPIGPs80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LbB26/btsNvdSu2NK/XdtsiZG8JN4ZnkKPIGPs80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LbB26/btsNvdSu2NK/XdtsiZG8JN4ZnkKPIGPs80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLbB26%2FbtsNvdSu2NK%2FXdtsiZG8JN4ZnkKPIGPs80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1582&quot; height=&quot;1107&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;1107&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgMCd6/btsNuHsLTQv/LSQfCbtX6NUF7uKjueIz80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgMCd6/btsNuHsLTQv/LSQfCbtX6NUF7uKjueIz80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgMCd6/btsNuHsLTQv/LSQfCbtX6NUF7uKjueIz80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgMCd6%2FbtsNuHsLTQv%2FLSQfCbtX6NUF7uKjueIz80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;1481&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1481&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RabbitMQ의 &lt;b&gt;Fanout Exchange&lt;/b&gt;로부터 메시지 수신&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;유효성 검사 &amp;rarr; 잘못된 메시지는 무시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;알림 설정이 활성화된 유저만 조회&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;각 유저에게 알림 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실패 시 로깅 처리&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@RabbitListener 기반 메시지 수신 : consume()&lt;/span&gt;&lt;/b&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RabbitMQ로부터 EVENT_OPEN_QUEUE에 수신된 메시지를 자동으로 처리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;메시지 유효성 검증 : isValid()&lt;/span&gt;&lt;/b&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;필수 필드(eventId, storeId)가 없는 메시지는 로그 경고 후 무시&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;알림 설정 유저만 선별 조회 : getUsers()&lt;/span&gt;&lt;/b&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;userService.getUsersWithAlarmEnabled() 을 통해 알림을 활성화한 유저만 필터링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;유저 목록 조회 실패 시 emptyList() 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;알림 전송 + 예외 대응 : sendNotification() &lt;/b&gt;&amp;rArr; 추후 MQ 기반 알림 전송 분리 적용 고려&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;알림 요청 객체(NotificationRequestDto) 생성 후 서비스 호출&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MQ 메시지 발행 및 소비가 정상적으로 수행되고 있는 것을 RabbitMQ 대시보드의 Message Rates 그래프로 확인한 결과이다.&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;688&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eitQpH/btsNx2Phwv7/h0YKXDd0RIjTqgJkyuZVM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eitQpH/btsNx2Phwv7/h0YKXDd0RIjTqgJkyuZVM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eitQpH/btsNx2Phwv7/h0YKXDd0RIjTqgJkyuZVM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeitQpH%2FbtsNx2Phwv7%2Fh0YKXDd0RIjTqgJkyuZVM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;276&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nvOMU/btsNw6LC68S/Hgdh1RITY4xSL4h0wxMAVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nvOMU/btsNw6LC68S/Hgdh1RITY4xSL4h0wxMAVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nvOMU/btsNw6LC68S/Hgdh1RITY4xSL4h0wxMAVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnvOMU%2FbtsNw6LC68S%2FHgdh1RITY4xSL4h0wxMAVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;98&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;98&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 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RabbitConfig - Queue, Exchange, Binding 설정&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RabbitConfig에 이벤트 오픈 관련 설정 등록한다.&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;1371&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O6fd0/btsNwMN2Kuo/8kakaaaQQK8EPnA4rZb1E1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O6fd0/btsNwMN2Kuo/8kakaaaQQK8EPnA4rZb1E1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O6fd0/btsNwMN2Kuo/8kakaaaQQK8EPnA4rZb1E1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO6fd0%2FbtsNwMN2Kuo%2F8kakaaaQQK8EPnA4rZb1E1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1371&quot; height=&quot;746&quot; data-origin-width=&quot;1371&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;eventOpenQueue()&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;큐 이름: EVENT_OPEN_QUEUE&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;durable = true: 메시지 브로커 재시작 후에도 큐 유지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;큐의 내구성을 보장하여 서비스 중단 없이 안정적인 운영 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;eventOpenExchange()&lt;/b&gt; : Fanout 방식의 브로드캐스트 Exchange 정의&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Exchange란 메세지를 어떤 큐로 보낼지 결정하는 중간 허브를 말한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Exchange 이름: EVENT_OPEN_EXCHANGE&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;메시지를 받은 모든 큐로 동시에 복제 전송&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;durable = true, autoDelete = false: 안정성과 명시적 해제를 모두 고려&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;eventOpenBinding()&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;eventOpenQueue()를 eventOpenExchange()에 바인딩&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Fanout 방식이므로 라우팅 키 불필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/391</guid>
      <comments>https://3uomlkh.tistory.com/391#entry391comment</comments>
      <pubDate>Wed, 23 Apr 2025 23:56:27 +0900</pubDate>
    </item>
    <item>
      <title>[트러블슈팅] 정원이 남아있는 상태에서 이벤트 신청이 안되는 오류</title>
      <link>https://3uomlkh.tistory.com/390</link>
      <description>&lt;h1 id=&quot;문제점&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제점&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이벤트 정원이 10명인데도 200명 신청 시 9명까지만 들어가는 문제가 발생했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또한 로그에서 &amp;ldquo;이벤트 정원이 초과되었습니다.&amp;rdquo;라는 메세지가 떴다.&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;1280&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/odHdp/btsNpAGOPuP/x78DgABHWB00qiFu584R5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/odHdp/btsNpAGOPuP/x78DgABHWB00qiFu584R5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/odHdp/btsNpAGOPuP/x78DgABHWB00qiFu584R5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FodHdp%2FbtsNpAGOPuP%2Fx78DgABHWB00qiFu584R5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;439&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;439&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis에는 이미 200명이 저장된 상태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DB는 한 명이 누락된 상태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DB에 존재하지 않는 ID로 다시 신청 &amp;rarr;&amp;nbsp;EVENT_ALREADY_JOINED&amp;nbsp;예외 발생&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;원인&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;원인&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;blockquote style=&quot;background-color: #f8f9fa; color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;&lt;b&gt;Redis와 DB의 정합성이 깨졌는데, 검증 기준은 Redis에만 의존하고 있기 때문이다.&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis는 ID를 이미 참여한 유저로 보고 있지만, 실제 DB에는 들어가지 않았다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 ZSet에 있는 것만 보고 판단하여 중복 신청으로 막아버린 것이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Zset에는 실패한 유저도 들어있기 때문에 100% 신뢰할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;왜 지금까지 이런 문제가 한 번도 없었는지는 잘 모르겠다.. &lt;/span&gt;&lt;/p&gt;
&lt;h1 id=&quot;해결-방법&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;해결 방법&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;validateEventCapacity()&amp;nbsp;메서드는 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;private void validateEventCapacity(String zsetKey, int limit) {
    Long current = redisTemplate.opsForZSet().zCard(zsetKey);
    
    if (current != null &amp;amp;&amp;amp; current &amp;gt;= limit) {
        throw new HandledException(ErrorCode.EVENT_FULL);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;메서드를 다음과 같이 수정하였다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;private void validateEventCapacity(String zsetKey, int limit, Event event) {
    Long current = redisTemplate.opsForZSet().zCard(zsetKey);
    
    // Redis 기준 정원 초과일 경우 -&amp;gt; DB 기준으로 한 번 더 확인
    if (current != null &amp;amp;&amp;amp; current &amp;gt;= limit) {
        long dbCount = eventJoinRepository.countByEvent(event);

        if (dbCount &amp;gt;= limit) {
            throw new HandledException(ErrorCode.EVENT_FULL);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;1280&quot; data-origin-height=&quot;539&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RmsF3/btsNqgAuYuI/YntaHCkFf10VwXUz4Ye600/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RmsF3/btsNqgAuYuI/YntaHCkFf10VwXUz4Ye600/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RmsF3/btsNqgAuYuI/YntaHCkFf10VwXUz4Ye600/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRmsF3%2FbtsNqgAuYuI%2FYntaHCkFf10VwXUz4Ye600%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;539&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;539&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;추가 +)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존 코드에서 DB Insert 실패 시 ZSet에서 제거하는 로직을 추가했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;private EventJoinResponseDto handleJoinLogic(Long eventId, AuthUser authUser) {
    User user = User.fromAuthUser(authUser);
    String zsetKey = EVENT_JOIN_PREFIX + eventId;

    Event event = eventRepository.findById(eventId)
            .orElseThrow(() -&amp;gt; new HandledException(ErrorCode.EVENT_NOT_FOUND));

    event.validateOpenStatus();
    validateEventNotAlreadyJoined(zsetKey, user);
    validateEventCapacity(zsetKey, event.getLimitPeople(), event);

    EventJoin eventJoin = saveEventJoin(event, user);

    log.info(&quot;이벤트 신청 성공: eventJoinId={}, user={}, event={}&quot;, eventJoin.getId(), user.getEmail(), eventId);
    return EventJoinResponseDto.fromEventJoin(eventJoin);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;수정한 코드는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;private EventJoinResponseDto handleJoinLogic(Long eventId, AuthUser authUser) {
    User user = User.fromAuthUser(authUser);
    String zsetKey = EVENT_JOIN_PREFIX + eventId;

    Event event = eventRepository.findById(eventId)
            .orElseThrow(() -&amp;gt; new HandledException(ErrorCode.EVENT_NOT_FOUND));

    event.validateOpenStatus();
    validateEventNotAlreadyJoined(zsetKey, user);
    validateEventCapacity(zsetKey, event.getLimitPeople(), event);

    try {
        EventJoin eventJoin = saveEventJoin(event, user);
        log.info(&quot;이벤트 신청 성공: eventJoinId={}, user={}, event={}&quot;, eventJoin.getId(), user.getEmail(), eventId);
        return EventJoinResponseDto.fromEventJoin(eventJoin);
    } catch (Exception e) {
        redisTemplate.opsForZSet().remove(zsetKey, String.valueOf(user.getId()));
        log.warn(&quot;DB insert 실패로 Redis 자리 반환: userId={}, eventId={}&quot;, user.getId(), eventId);
        throw e;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1번 ~ 10번까지는 DB에 들어간 순서 그대로이고, 나머지는 순간적인 동시 처리 때문에 남아있는 것 같다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그러나 이전에&amp;nbsp;ZRANGE event:join:1 0 -1&amp;nbsp;을 했을 때 는 200명이 그대로 들어가 있었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;로직 수정 후 ZSet에서 실제 정원과 비슷한 수준으로 유지되고 있다.&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;1280&quot; data-origin-height=&quot;1411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sR9l7/btsNprDnxHR/C1HXOduil5JBtmk2Cu62AK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sR9l7/btsNprDnxHR/C1HXOduil5JBtmk2Cu62AK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sR9l7/btsNprDnxHR/C1HXOduil5JBtmk2Cu62AK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsR9l7%2FbtsNprDnxHR%2FC1HXOduil5JBtmk2Cu62AK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;1411&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1411&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;200명(신청 시도 유저 전부) &amp;rarr; 22명(정상적으로 반영된 유저 + 미처 삭제 되지 못한 유)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;지삐띠가 만들어준&amp;nbsp;ZRANGE&amp;nbsp;수치 변화&amp;hellip;&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;1280&quot; data-origin-height=&quot;793&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csiROH/btsNo9bUstJ/SLqoBZRRtaSuhHoRO49m41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csiROH/btsNo9bUstJ/SLqoBZRRtaSuhHoRO49m41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csiROH/btsNo9bUstJ/SLqoBZRRtaSuhHoRO49m41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsiROH%2FbtsNo9bUstJ%2FSLqoBZRRtaSuhHoRO49m41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;793&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;793&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 id=&quot;추가-공부&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;추가 공부&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;추후 MQ를 도입한다면 아래와 같은 흐름으로 진행될 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;1) 유저가 이벤트 신청
2) Redis ZSet에 userId 추가 시도
	- zCard 기준으로 정원 초과 여부 확인
	- 정원 초과 시 예외
3) Redis에 성공적으로 추가되면 MQ에 메세지 보냄(&quot;이벤트 신청했다.&quot;)
4) Redis만 보고 응답을 빠르게 반환
---
5) MQ는 이 메세지를 큐에 저장
---
6) Consumer(이벤트 신청 처리기)가 큐로부터 메시지를 수신
7) 메시지 처리
	- 이벤트 존재 여부 확인
	- 유저 정보 확인
	- DB에 Insert 시도
8) Consumer 처리 결과에 따라 분기
	- DB Insert 성공
	- DB Insert 실패 &amp;rarr; Redis ZSet에서 해당 userId 제거&lt;/code&gt;&lt;/pre&gt;</description>
      <category>redis</category>
      <category>Redisson</category>
      <category>Spring</category>
      <category>동시성제어</category>
      <category>트러블슈팅</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/390</guid>
      <comments>https://3uomlkh.tistory.com/390#entry390comment</comments>
      <pubDate>Fri, 18 Apr 2025 19:48:16 +0900</pubDate>
    </item>
    <item>
      <title>캐싱(caching)이란? / Redis(Remote Dictionary Server)란? / 데이터 캐싱 전략 정리</title>
      <link>https://3uomlkh.tistory.com/389</link>
      <description>&lt;div style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;
&lt;div style=&quot;color: #212529;&quot;&gt;
&lt;div&gt;
&lt;h2 id=&quot;캐싱caching이란&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;캐싱(caching)이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis에 대해 알아보기 전, 먼저 캐시 &amp;amp; 캐싱이 무엇인지 알아보자! 파레토의 법칙에 따라 자주 사용되는 20%의 데이터를 미리 캐싱해둔다면 효과적인 성능 향상을 기대할 수 있다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;캐시(Cache)&lt;/b&gt;&amp;nbsp;: 원본 저장소보다 데이터를 더 빠르고 효율적으로 가져올 수 있는 임시 데이터 저장소.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;캐싱(Caching)&lt;/b&gt;&amp;nbsp;: 캐시에 접근해 데이터를 빠르게 가져오는 방식.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자주 사용되면서 변경이 적은 데이터에 적합하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;유실되어도 크게 문제가 없는 데이터에 적합하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DB와 함께 사용 시 데이터 정합성 문제를 고려해야한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;데이터-캐싱-전략&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단순히 &quot;캐싱&quot;만 한다고 성능이 무조건 보장되는 것은 아니다.&amp;nbsp;&lt;b&gt;어떻게 캐싱하느냐&lt;/b&gt;가 중요하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;대표적인 데이터 캐싱 전략을 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;cache-asidelook-aside-lazy-loading&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Cache Aside(=Look Aside, Lazy Loading)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시에서 데이터를 확인하고, 없다면 DB를 통해 조회해오는 방식.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Cache Aside는 Redis를 캐시로 쓸 때 가장 일반적으로 사용하는 방법으로, 데이터에 대한 요청이 들어오면 먼저 캐시를 확인한다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;캐시에 데이터가 있으면(cache hit) 캐시에서 데이터를 가져온다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;캐시에 데이터가 없으면(cache miss) DB에서 데이터를 가져와 캐시에 저장 후, 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;캐시에 문제가 생기면 Db로 요청을 위임할 수 있어 캐시 미스 제어가 쉽다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터 불일치 가능성 : 캐시된 후 DB의 데이터가 변경되면 캐시는 오래된 데이터를 반환할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;write-around&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Write Around&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 쓸 때 DB에만 저장하고 캐시에는 반영하지 않는 방식.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Write Around는 한 번 쓰고 자주 읽지는 않는 경우에 적합하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DB에 바로 쓰기 때문에 성능이 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;불필요한 데이터를 캐시에 올리지 않고 바로 쓰기 때문에 리소스를 아낄 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;캐시와 DB의 정합성 유지가 어렵다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;write-through&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Write Through&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 쓸 때 DB와 캐시 둘 다 동시에 갱신하는 방식.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Write Through는 데이터에 대한 요청이 들어오면 캐시와 DB에 모두 기록된다. 따라서 캐시에는 항상 최신 데이터가 저장된다. 데이터의 일관성이 중요할 때 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;캐시가 항상 최신 정보를 가진다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;캐시와 DB가 항상 동기화 되어서 데이터 일관성을 보장한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;항상 두 번의 쓰기가 발생하므로 성능 문제에 유의해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;write-backwrite-behind&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Write Back(=Write Behind)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 먼저 캐시에 저장하고, 이후 비동기로 DB에 반영하는 방식.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Write Back은 캐시에 한꺼번에 기록 후, 나중에 DB에 한꺼번에 저장한다. 이때 스케줄링을 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DB에 대한 쓰기 작업 수를 줄여(한 번의 쿼리로 삽입) 성능이 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단점&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;-DB에 저장되기 전에 캐시에 오류가 나면 데이터 손실이 발생할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;성능 개선을 한다고 꼭 바로 캐시를 적용하려고 하지 않아도 된다. 우선&amp;nbsp;&lt;b&gt;SQL 튜닝&lt;/b&gt;을 통해 기본적으로 성능을 향상시킬 수 있다. 또한 SQL 튜닝은 추가적인 시스템 구축 없이도 적용할 수 있기 때문에 가장 먼저 고려해보도록 하자!&lt;/span&gt;&lt;/p&gt;
&lt;h1 id=&quot;redisremote-dictionary-server&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Redis(Remote Dictionary Server)&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 데이터 처리 속도가 매우 빠른 Key-Value 기반의 NoSQL 데이터베이스이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis는&amp;nbsp;&lt;b&gt;인메모리(in-memory)&lt;/b&gt;에 모든 데이터를 저장하므로&amp;nbsp;&lt;b&gt;데이터의 처리 성능이 매우 빠르다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다양한 자료구조를 제공한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;싱글 스레드로 동작해 한 번에 하나의 명령만을 처리하기 때문에 Race Condition이 거의 발생하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;메모리에 저장된 데이터를 디스크에 영속화하는 기능을 제공한다.(RDB, AOF)&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis는 주로 캐싱에 사용된다는 것만 알고 있었는데, 검색해보니 아주 많은 사용 사례가 있었다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;캐싱&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;세션 관리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실시간 분석 및 통계&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;메시지 큐&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;지리공간 인덱싱&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;속도 제한&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실시간 채팅 및 메시징&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;가장 먼저&amp;nbsp;&lt;b&gt;캐싱을 통한 데이터 조회 성능 향상&lt;/b&gt;에 대해 학습하면서 자연스럽게 다른 기능을 익혀보는 것이 좋다. 또한 채용 공고에 자주 등장하는 &amp;lsquo;대용량 트래픽 처리 경험&amp;rsquo;을 위해 필수적인 것이 바로 Redis의 캐싱 기능이다!&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;redis-기본-명령어&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis 기본 명령어&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 id=&quot;데이터-key-value-저장하기&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터 (Key-Value) 저장하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# set [key 이름] [value]
$ set gom:name &quot;gom gom&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key로-value-조회하기&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Key로 Value 조회하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;# get [key 이름]
$ get gom:name&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;저장된-모든-key-조회하기&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;저장된 모든 Key 조회하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;elixir&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;$ keys *&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단, Redis는&amp;nbsp;&lt;b&gt;싱글 스레드&lt;/b&gt;로 동작하기 때문에&amp;nbsp;keys *와 같이 모든 키를 탐색하는&amp;nbsp;&lt;b&gt;O(N) 복잡도의 작업은 성능에 큰 영향&lt;/b&gt;을 줄 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;key로-데이터-삭제하기&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;key로 데이터 삭제하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;maxima&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;# del [key 이름]
$ del gom:name&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;데이터-저장-시-만료시간ttl-정하기&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터 저장 시 만료시간(TTL) 정하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;gams&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;# set [key 이름] [value] ex [만료 시간(초)]
$ set gom:pet dog ex 30&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;데이터-저장-시-만료시간ttl-정하기-1&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터 저장 시 만료시간(TTL) 정하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;# ttl [key 이름]
$ ttl gom:pet&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;447&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmllVa/btsNrm2Yj10/H1ji8m29qMJyIOeHKQzkk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmllVa/btsNrm2Yj10/H1ji8m29qMJyIOeHKQzkk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmllVa/btsNrm2Yj10/H1ji8m29qMJyIOeHKQzkk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmllVa%2FbtsNrm2Yj10%2FH1ji8m29qMJyIOeHKQzkk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;388&quot; height=&quot;447&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;447&quot;/&gt;&lt;/span&gt;&lt;/figure&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;만료 시간이 몇 초 남았는지 반환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;-2&amp;nbsp;: TTL이 만료된 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;-1&amp;nbsp;: 키 값은 존재하지만, TTL이 설정되어있지 않은 경우&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;redis-key-네이밍-컨벤션&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis Key 네이밍 컨벤션&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 콜론(:)을 활용해 계층적으로 의미를 구분해 사용 &amp;rarr; 가독성, 일관성, 검색 및 필터링 용이성, 확장&lt;/p&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;users:100:profile&amp;nbsp;: 사용자들 중에서 아이디가 100인 사용자의 프로필&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;products:123:details&amp;nbsp;: 상품들 중에서 아이디가 123인 상품의 세부사항&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;-참고-자료&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  참고 자료&lt;/b&gt;&lt;/span&gt;&lt;/h1&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a style=&quot;color: #12b886;&quot; href=&quot;https://thinhdanggroup.github.io/caching-stategies/&quot;&gt;Mastering Caching Strategies: A Comprehensive Guide&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a style=&quot;color: #12b886;&quot; href=&quot;https://www.youtube.com/playlist?list=PLtUgHNmvcs6qoVrxB5jzZ4meINz_KL-Bl&quot;&gt;[인프런] 비전공자도 이해할 수 있는 Redis 입문/실전&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a style=&quot;color: #12b886;&quot; href=&quot;https://youtu.be/tVZ15cCRAyE?si=wA6ntOSNbgr_UfWv&quot;&gt;[10분 테코톡] 저문, 라온의 Cache &amp;amp; Redis&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>TIL</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/389</guid>
      <comments>https://3uomlkh.tistory.com/389#entry389comment</comments>
      <pubDate>Thu, 17 Apr 2025 00:08:38 +0900</pubDate>
    </item>
    <item>
      <title>[트러블슈팅] 순환 참조 오류(Circular Dependency)</title>
      <link>https://3uomlkh.tistory.com/388</link>
      <description>&lt;h1 id=&quot;문제점&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제점&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;1280&quot; data-origin-height=&quot;423&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nXWnF/btsNkn2mJ7R/7RmdxfPNY24vQ9zsKbaZO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nXWnF/btsNkn2mJ7R/7RmdxfPNY24vQ9zsKbaZO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nXWnF/btsNkn2mJ7R/7RmdxfPNY24vQ9zsKbaZO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnXWnF%2FbtsNkn2mJ7R%2F7RmdxfPNY24vQ9zsKbaZO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;423&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;423&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 id=&quot;원인&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;원인&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Bean 초기화 시에 순환 참조가 생기면서 앱이 실행되지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EventService&amp;nbsp;&amp;rarr;&amp;nbsp;EventJoinService를&amp;nbsp;@RequiredArgsConstructor로 주입&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EventJoinService&amp;nbsp;&amp;rarr;&amp;nbsp;EventService를 또&amp;nbsp;@RequiredArgsConstructor로 주입&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서로 의존성을 주입하고 있어서 다음과 같은 순환 의존 구조가 된 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;EventController
  &amp;rarr; EventService
    &amp;rarr; EventJoinService
      &amp;rarr; EventService (다시 참조)&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&quot;해결-과정&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;해결 과정&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존에&amp;nbsp;EventJoinService에서 사용하고 있던&amp;nbsp;EventService를&amp;nbsp;EventRepository로 바꿨다.&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;1280&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0wkU0/btsNoxWDuw0/3yQqJ6FNylnaRtiBdYLxWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0wkU0/btsNoxWDuw0/3yQqJ6FNylnaRtiBdYLxWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0wkU0/btsNoxWDuw0/3yQqJ6FNylnaRtiBdYLxWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0wkU0%2FbtsNoxWDuw0%2F3yQqJ6FNylnaRtiBdYLxWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;186&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EventService의&amp;nbsp;getEvent()&amp;nbsp;메서드를 호출하는 구조였는데, 직접&amp;nbsp;eventRepository.findById()&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;1280&quot; data-origin-height=&quot;69&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/prXsC/btsNoNrnLlX/iwjC06yofZVedrnWU7n6n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/prXsC/btsNoNrnLlX/iwjC06yofZVedrnWU7n6n0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/prXsC/btsNoNrnLlX/iwjC06yofZVedrnWU7n6n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FprXsC%2FbtsNoNrnLlX%2FiwjC06yofZVedrnWU7n6n0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;69&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;69&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Event와&amp;nbsp;User는&amp;nbsp;@ManyToOne&amp;nbsp;&amp;rarr; 즉,&amp;nbsp;EventJoin이 소유자&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;비즈니스적으로&amp;nbsp;EventJoin을 만들기 위해&amp;nbsp;Event가 필요함.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그러니&amp;nbsp;eventRepository.findById()&amp;nbsp;혹은&amp;nbsp;findByIdForUpdate()를 호출하는 건 자연스러움.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EventJoinService&amp;nbsp;를 Event의 하위 개념이라는 것을 잊고 작업해서 실수를 한 것 같다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;구조를 생각해보면&amp;nbsp;User&amp;nbsp;&amp;harr;&amp;nbsp;EventJoin&amp;nbsp;&amp;harr;&amp;nbsp;Event&amp;nbsp;와 같은데,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EventJoin은 &quot;누가 어떤 이벤트에 참여했는가&quot;를 저장하는&amp;nbsp;&lt;b&gt;이벤트 신청 테이블로,&lt;/b&gt;&amp;nbsp;유저와 이벤트 사이의 중간 테이블로 생각해야 한다. 또 단순 조회만을 하고 있기 때문에 굳이&amp;nbsp;EventService를 쓸 필요는 없다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>트러블슈팅</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/388</guid>
      <comments>https://3uomlkh.tistory.com/388#entry388comment</comments>
      <pubDate>Wed, 16 Apr 2025 23:54:05 +0900</pubDate>
    </item>
    <item>
      <title>TableNow 개발 회고 #1 &amp;ndash; API 설계, 데이터 모델링, 시스템 아키텍처와 초기 구현</title>
      <link>https://3uomlkh.tistory.com/387</link>
      <description>&lt;h1 id=&quot;️최종-프로젝트-시작&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; ️최종 프로젝트 시작!&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;드디어 최종 프로젝트를 시작했다. 사실 벌써 2주차가 끝났지만 정신없이 달려오다보니 블로그에 기록을 하지 못했다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;한 주가 끝날 때마다 꾸준히 기록하는 습관을 가지자.✍ &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;우리 팀은 테이블링 서비스를 주제로 정했다!&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;프로젝트명 : TableNow (테이블나우)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;주제 : 식당을 예약하고 방문할 수 있는 실시간 예약 관리 시스템&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  1 ~ 2주차 일정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;S.A 작성 후 피드백 받기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기본 기능 개발 완료&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기본 기능 개발 후 코드리뷰 받기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;핵심-기능&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;핵심 기능&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;우리 팀의 핵심 기능은 다음과 같이 정리할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #212529; text-align: start; border-collapse: collapse; width: 55.2325%; height: 298px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td style=&quot;width: 29.7252%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;인증/보안&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.1585%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring Security + JWT&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Refresh Token 저장 및 관리(Redis)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;OAuth2.0 로그인 연동&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td style=&quot;width: 29.7252%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;검색/캐싱&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.1585%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;인기 검색어 캐싱(Redis)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Elasticsearch&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td style=&quot;width: 29.7252%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;동시성 제어&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.1585%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redisson&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis Sorted Set&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td style=&quot;width: 29.7252%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MQ&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.1585%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RabbitMQ&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;나는 예약/이벤트 도메인을 맡았고, 동시성 제어는 처음이어서 핵심 기술에 대해 고민이 많았다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기술적의사결정 과정은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;redisson-분산-락&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Redisson (분산 락)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  목적:&lt;/b&gt;&amp;nbsp;예약 및 이벤트 요청 시 중복 처리를 방지하기 위한 분산 락 구현&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[선택 이유]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis 기반으로 빠르고 가볍게 락을 제어 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redisson은 다양한 락 타입 지원 (Fair, Reentrant 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;TTL 설정으로 데드락 방지 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring Boot와의 통합이 쉬움&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[다른 기술과의 비교]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;DB 트랜잭션 락 (SELECT ... FOR UPDATE):&lt;/b&gt;&amp;nbsp;단일 DB 환경에서는 가능하지만, 분산 환경에서는 사용 불가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;ZooKeeper:&lt;/b&gt;&amp;nbsp;고신뢰 락을 제공하지만 운영 복잡도가 매우 높음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[결론]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;복잡한 락 시스템을 쓰기보다, Redis 기반의 간단하면서도 안정적인 락의 필요성을 느껴 Redisson으로 결정했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또한 다른 도메인에서도 Redis를 이미 사용하고 있으므로 추가적인 리스크 없이 도입이 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;redis-sorted-set---시간-기반-트리거-처리&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Redis Sorted Set - 시간 기반 트리거 처리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  목적:&lt;/b&gt;&amp;nbsp;리마인드 알림, 이벤트 오픈과 같이 특정 시간에 발생해야 하는 작업을 스케줄링&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[선택 이유]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Sorted Set의 score에 timestamp를 사용해 시간순 정렬 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;주기적인 polling으로 실행 대상만 빠르게 추출 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;배치보다 실시간에 가깝고, 구현과 운영이 간편함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis 인프라를 재사용할 수 있어 도입 비용 거의 없음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[다른 기술과의 비교]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Quartz Scheduler:&lt;/b&gt;&amp;nbsp;정기적인 작업을 정밀하게 관리해야 할 때 사용함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;RabbitMQ Delayed Queue:&lt;/b&gt;&amp;nbsp;구현 복잡하고 유연성 낮음, 지연 시간 기반(ex. 5분 뒤, 10분 뒤)이라 특정 시간 트리거에는 불편함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;일반 DB + 배치:&lt;/b&gt;&amp;nbsp;실시간 반응이 어렵고 리소스 낭비가 될 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[결론]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;시간을 score로 설정하여 정확한 시간에 맞춰서 작업을 트리거할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서 리마인드 알림, 이벤트 오픈 시간 도달 시 트리거(가게 상태 변경) 작업에 적합하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또한 이벤트 오픈 시 실시간 처리가 필요하므로 메모리 기반의 Redis가 효율적이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;rabbitmq---비동기-이벤트-전파&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;RabbitMQ - 비동기 이벤트 전파&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  목적:&lt;/b&gt;&amp;nbsp;예약 완료, 리마인드 알림, 이벤트 오픈 알림을 다른 도메인으로 안전하게 전파&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[선택 이유]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;메시지 전송의 신뢰성 보장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;비동기 메시징 시스템으로 각 도메인 간 느슨한 결합 유지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다양한 메시징 패턴 (Fanout, Topic 등)을 제공해 유연한 라우팅 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[다른 기술과의 비교]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Kafka:&lt;/b&gt;&amp;nbsp;대용량 데이터 처리, 실시간, 고성능, 고가용성을 제공하지만 로그 기반 스트리밍에 초점, 복잡성 및 설정이 과도함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Redis Pub/Sub:&lt;/b&gt;&amp;nbsp;설정이 간단하고 미들웨어가 없어 가볍지만 메시지 유실 가능성이 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;AWS SQS:&lt;/b&gt;&amp;nbsp;클라우드 종속적이고 비용이 발생할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[결론]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MQ를 사용하는 것은 이번이 처음이지만, 대용량 데이터 처리나 로그 추적, 재처리가 필요한 상황은 아니었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다만 리마인드 알림뿐 아니라 빈자리 알림, 이벤트 오픈 알림처럼&amp;nbsp;&lt;b&gt;선착순으로 처리되어야 하는 이벤트들이 존재&lt;/b&gt;했기 때문에&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;메시지의 정확한 도착 보장이 무엇보다 중요했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이에 따라 Kafka보다 속도는 느리더라도,&amp;nbsp;&lt;b&gt;정확한 메시지 전달과 유연한 라우팅이 가능&lt;/b&gt;한 RabbitMQ를 선택하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;h1 id=&quot;와이어프레임&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;와이어프레임&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GiwH4/btsNm4g5iqo/Z0AVUYuovqnFSkKpz7tCvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GiwH4/btsNm4g5iqo/Z0AVUYuovqnFSkKpz7tCvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GiwH4/btsNm4g5iqo/Z0AVUYuovqnFSkKpz7tCvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGiwH4%2FbtsNm4g5iqo%2FZ0AVUYuovqnFSkKpz7tCvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;853&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 id=&quot;api-설계&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;API 설계&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;노션으로 팀원들과 작성한&amp;nbsp;&lt;a style=&quot;color: #12b886;&quot; href=&quot;https://www.notion.so/1d5d5efee5f18164925df55e2d11dc23?v=1d5d5efee5f181c08484000cf31e0f5f&amp;amp;pvs=4&quot;&gt;API 명세서&lt;/a&gt;다. 분류, 담당자, 진행사항, 메서드, 기능, URI, 응답/요청 헤더 및 바디, 상태 코드 등으로 이루어져 있다. RESTful한 API를 설계하고자 했다.&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;1280&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/unkPd/btsNmCSUOQ1/SY6r3dD1ZrxNXmcaduS2A1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/unkPd/btsNmCSUOQ1/SY6r3dD1ZrxNXmcaduS2A1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/unkPd/btsNmCSUOQ1/SY6r3dD1ZrxNXmcaduS2A1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FunkPd%2FbtsNmCSUOQ1%2FSY6r3dD1ZrxNXmcaduS2A1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;547&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 id=&quot;데이터-모델링&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;데이터 모델링&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;초기 ERD는 리뷰, 댓글, 좋아요, 즐겨찾기 등 부가적인 기능이 많았다. 하지만 비즈니스적으로 다가가 기능을 추가하는 것보다는&amp;nbsp;&lt;b&gt;기술적인 도전을 하는 것에 초점을 맞추기로 했다.&lt;/b&gt;&amp;nbsp;따라서 테이블 수를 줄이고 각자 핵심적인 1 ~ 2개의 도메인만 맡아 진행했다.&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;1280&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHVUkN/btsNmAHyI4s/kXoSVz0PzITFKOgGLEWhm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHVUkN/btsNmAHyI4s/kXoSVz0PzITFKOgGLEWhm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHVUkN/btsNmAHyI4s/kXoSVz0PzITFKOgGLEWhm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHVUkN%2FbtsNmAHyI4s%2FkXoSVz0PzITFKOgGLEWhm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;341&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아마 기술 고도화가 진행되면 좀 더 수정될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;h1 id=&quot;아키텍처-설계&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;아키텍처 설계&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;우리 팀은 무중단 배포 방식을 원했는데, 현실적인 상황에 맞춰 EC2 1대에 컨테이너 2개를 띄우고 그 앞단에 Nginx와 같은 웹서버를 두는 방향으로 정했다. Blue-Green 방식으로 진행하고자 한다.&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;1280&quot; data-origin-height=&quot;713&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBeksH/btsNoaUprl5/MshVnpALUlv2RbfC4vyaiK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBeksH/btsNoaUprl5/MshVnpALUlv2RbfC4vyaiK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBeksH/btsNoaUprl5/MshVnpALUlv2RbfC4vyaiK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBeksH%2FbtsNoaUprl5%2FMshVnpALUlv2RbfC4vyaiK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;713&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;713&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 id=&quot;프로젝트-구조&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;프로젝트 구조&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;table-now
├── .github
│   └── ISSUE_TEMPLATE
│       ├── feature-request-issue-template.md
│       └── PULL_REQUEST_TEMPLATE.md
├── src
│   ├── main
│   │   ├── java
│   │   │   └── org.example.tablenow
│   │   │       ├── domain
│   │   │       │   ├── auth
│   │   │       │   ├── category
│   │   │       │   ├── event
│   │   │       │   ├── image
│   │   │       │   ├── notification
│   │   │       │   ├── payment
│   │   │       │   ├── reservation
│   │   │       │   ├── store
│   │   │       │   ├── user
│   │   │       │   └── waitlist
│   │   │       ├── global
│   │   │       │   ├── annotation
│   │   │       │   ├── config
│   │   │       │   ├── dto
│   │   │       │   ├── entity
│   │   │       │   ├── exception
│   │   │       │   ├── filter
│   │   │       │   ├── security
│   │   │       │   └── util
│   │   │       └── TableNowApplication.java
│   │   └── resources
│   │       ├── application.yml
│   │       ├── application-local.yml
│   │       └── application-test.yml
│   └── test
├── Dockerfile
├── docker-compose.yml
├── .env
├── .env.local
├── .env.test
├── .gitignore
├── .gitattributes
├── build.gradle
├── gradlew
├── gradlew.bat
├── settings.gradle&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  환경변수 관리는 어떻게 할까?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;우리는 환경마다 다른 설정을 유연하게 관리하기 위해,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring Boot의&amp;nbsp;&lt;b&gt;application.yml&lt;/b&gt;&amp;nbsp;계층 구조와&amp;nbsp;&lt;b&gt;.env&lt;/b&gt;&amp;nbsp;파일을 함께 사용하는 방식을 선택했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;application.yml&amp;nbsp;: &lt;b&gt;공통 설정&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;application-local.yml&amp;nbsp;: &lt;b&gt;로컬 개발 환경 설정&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;application-test.yml&amp;nbsp;: &lt;b&gt;테스트 환경 설정&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 설정 파일들과 연동되는&amp;nbsp;&lt;b&gt;.env&lt;/b&gt;,&amp;nbsp;&lt;b&gt;.env.local&lt;/b&gt;,&amp;nbsp;&lt;b&gt;.env.test&lt;/b&gt;&amp;nbsp;파일을 각각 만들어&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;DB 접속 정보, Redis 설정, AWS 자격 증명&lt;/b&gt;&amp;nbsp;등 외부 노출되면 안 되는 민감한 값을 여기에 따로 분리해 관리하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;예를 들어&amp;nbsp;.env.local에는 아래와 같은 값들이 들어간다.&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;977&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo8FGB/btsNnUqZ6Ag/03DPkXewCtMklI9uOTcbuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo8FGB/btsNnUqZ6Ag/03DPkXewCtMklI9uOTcbuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo8FGB/btsNnUqZ6Ag/03DPkXewCtMklI9uOTcbuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbo8FGB%2FbtsNnUqZ6Ag%2F03DPkXewCtMklI9uOTcbuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;977&quot; height=&quot;298&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 하면 민감한 정보는 Git에 올라가지 않고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;각 환경별로&amp;nbsp;.env만 바꿔주면 바로 설정이 적용되어 훨씬 안전하고 편하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; &amp;nbsp;EnvFile&amp;nbsp;플러그인 함께 사용하기&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EnvFile&amp;nbsp;플러그인을 사용하면&amp;nbsp;Run/Debug Configuration에&amp;nbsp;.env&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;1280&quot; data-origin-height=&quot;955&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9IyEC/btsNocdCf7m/HssQEh0WotsJChBkBHTCAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9IyEC/btsNocdCf7m/HssQEh0WotsJChBkBHTCAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9IyEC/btsNocdCf7m/HssQEh0WotsJChBkBHTCAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9IyEC%2FbtsNocdCf7m%2FHssQEh0WotsJChBkBHTCAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;955&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;955&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아래처럼 Run 설정에서 &quot;Enable EnvFile&quot; 옵션을 체크하고 원하는&amp;nbsp;.env&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;1203&quot; data-origin-height=&quot;1039&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPCmXs/btsNmLhNkcS/4uE5IpNqtNhVfrUqjKLLyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPCmXs/btsNmLhNkcS/4uE5IpNqtNhVfrUqjKLLyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPCmXs/btsNmLhNkcS/4uE5IpNqtNhVfrUqjKLLyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPCmXs%2FbtsNmLhNkcS%2F4uE5IpNqtNhVfrUqjKLLyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1203&quot; height=&quot;1039&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;1039&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 id=&quot;기능-구현&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기능 구현&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2주차는 기술 고도화를 위한&amp;nbsp;&lt;b&gt;기본 기능을 개발&lt;/b&gt;하고, 각자 받은&amp;nbsp;&lt;b&gt;코드리뷰를 바탕으로 리팩토링을 진행&lt;/b&gt;했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;예약 생성, 조회, 수정, 취소, 처리 기능 구현&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이벤트 등록, 조회, 수정, 삭제, 신청(참여) 기능 구현&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;구현한 부분을 일일이 적기엔 양이 많고 기본적인 코드만 있어서&amp;nbsp;&lt;b&gt;내가 신경 쓴 부분, 리팩토링 한 부분, 고민했던 점&lt;/b&gt;&amp;nbsp;등을 간단히 적어본다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;검증-책임-분리&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;검증 책임 분리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;예약 기능에서는 예약 가능 여부, 중복 여부, 매장 상태 등의 다양한 조건을 검증해야 했다. 조건이 복잡해질수록 코드가 비대해질 수 있기 때문에, 검증 책임을 메서드 단위로 분리하여&amp;nbsp;&lt;b&gt;클린 코드 원칙(단일 책임, 가독성, 유지보수성)&lt;/b&gt;을 고려해 설계했다. 이 부분은 튜터님께서도 잘 구성했다고 긍정적인 피드백을 주셨다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;1280&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m0iCa/btsNnjrzliE/nPS1cOkIKbWMcg27NmH1EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m0iCa/btsNnjrzliE/nPS1cOkIKbWMcg27NmH1EK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m0iCa/btsNnjrzliE/nPS1cOkIKbWMcg27NmH1EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm0iCa%2FbtsNnjrzliE%2FnPS1cOkIKbWMcg27NmH1EK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;320&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;1091&quot; data-origin-height=&quot;637&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmTaIv/btsNnlQpmaa/lG8z6XwuJD2dhNdTWs7NBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmTaIv/btsNnlQpmaa/lG8z6XwuJD2dhNdTWs7NBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmTaIv/btsNnlQpmaa/lG8z6XwuJD2dhNdTWs7NBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmTaIv%2FbtsNnlQpmaa%2FlG8z6XwuJD2dhNdTWs7NBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1091&quot; height=&quot;637&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;637&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;도메인이-책임지는-예약-변경-로직&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;도메인이 책임지는 예약 변경 로직&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;예약 상태 변경 로직은 단순히 서비스 레이어에서 처리하는 것이 아니라,&amp;nbsp;&lt;b&gt;예약 도메인 자체가 그 책임을 가지도록 구성&lt;/b&gt;했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;예를 들어&amp;nbsp;tryCancel()&amp;nbsp;메서드는 단순한 setter가 아니라, 예약이 취소 가능한 상태인지 자체 검증을 포함한 명확한 도메인 행위로 정의되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또한 메서드 네이밍도&amp;nbsp;tryCancel,&amp;nbsp;updateStatus처럼 의도를 잘 드러내는 이름으로 구성하여 코드를 읽는 사람도 도메인 흐름을 쉽게 이해할 수 있도록 했다.&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;960&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SDIAd/btsNm1EI91i/fGhVUPlZ6vIJGE1Tkw9b01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SDIAd/btsNm1EI91i/fGhVUPlZ6vIJGE1Tkw9b01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SDIAd/btsNm1EI91i/fGhVUPlZ6vIJGE1Tkw9b01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSDIAd%2FbtsNm1EI91i%2FfGhVUPlZ6vIJGE1Tkw9b01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;323&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;750&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AmqE4/btsNobseLbs/xjUYFLmdugidXwOmgZTLDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AmqE4/btsNobseLbs/xjUYFLmdugidXwOmgZTLDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AmqE4/btsNobseLbs/xjUYFLmdugidXwOmgZTLDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAmqE4%2FbtsNobseLbs%2FxjUYFLmdugidXwOmgZTLDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;479&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서비스 로직에서 직접 상태를 변경하는 대신,&amp;nbsp;&lt;b&gt;도메인 객체에게 그 책임을 위임함&lt;/b&gt;으로써 비즈니스 로직을 더욱 명확하고 견고하게 관리할 수 있었다. 물론 이런 방식이 모든 상황에 정답은 아니지만, 상황에 따라 잘 판단해서 사용하면 좋을 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;예약-시간-검증-리팩토링&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;예약 시간 검증 리팩토링&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;초기에는 예약 시간의 유효성을 서비스단에서 메서드로 직접 검증했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아래와 같이&amp;nbsp;validateReservedAtHalfHour()&amp;nbsp;메서드를 통해 정시(00분) 또는 30분 단위가 아닌 경우 예외를 발생시키는 방식이었다.&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;844&quot; data-origin-height=&quot;189&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mz8p0/btsNn2I59jn/xFZSrRfQCeLJi3FYKyKJ3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mz8p0/btsNn2I59jn/xFZSrRfQCeLJi3FYKyKJ3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mz8p0/btsNn2I59jn/xFZSrRfQCeLJi3FYKyKJ3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmz8p0%2FbtsNn2I59jn%2FxFZSrRfQCeLJi3FYKyKJ3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;844&quot; height=&quot;189&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;189&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 튜터님으로부터&amp;nbsp;&lt;b&gt;&quot;요청 DTO의 단일 필드 유효성은 컨트롤러 단에서 처리하는 것이 적절하다&quot;&lt;/b&gt;&amp;nbsp;는 피드백을 받고, 해당 검증 책임을 요청 객체 쪽으로 이동했다. 이에 따라 다른 팀원 분이 하신 방식을 따라 커스텀 어노테이션&amp;nbsp;@HalfHourOnly를 이용해 컨트롤러 진입 시점에서 입력값의 형식을 검증할 수 있도록 리팩토링했다.&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;709&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sJ8Tr/btsNobeGyIU/jeJi7eKFg4aDE9u4HSZHk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sJ8Tr/btsNobeGyIU/jeJi7eKFg4aDE9u4HSZHk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sJ8Tr/btsNobeGyIU/jeJi7eKFg4aDE9u4HSZHk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsJ8Tr%2FbtsNobeGyIU%2FjeJi7eKFg4aDE9u4HSZHk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;709&quot; height=&quot;392&quot; data-origin-width=&quot;709&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;검증 로직을 컨트롤러 단으로 이동시킴으로써&amp;nbsp;&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;847&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/maSrL/btsNoby0c6N/3rBu6ORjV9eWgadrxKSxZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/maSrL/btsNoby0c6N/3rBu6ORjV9eWgadrxKSxZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/maSrL/btsNoby0c6N/3rBu6ORjV9eWgadrxKSxZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmaSrL%2FbtsNoby0c6N%2F3rBu6ORjV9eWgadrxKSxZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;847&quot; height=&quot;476&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;db-lock-구현&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;DB Lock 구현&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;처음에는 정말 기본 기능만 구현한 후 Redisson을 적용하려고 했는데, 막상 진행하려니 아주 기본적인 구조여서 베이스 라인으로 하기에는 부실한 것 같다는 생각이 들었다. 고민하다가 튜터님께 질문하니&amp;nbsp;&lt;b&gt;&quot;현재 상황과 Redisson 락 적용을 비교하는 것은 크게 의미가 없다&quot;&lt;/b&gt;&amp;nbsp;라고 하셨다. 따라서 추후 비교를 한다면&amp;nbsp;&lt;b&gt;DB 락 vs Redisson 분산 락&lt;/b&gt;을 해보는 것이 더 낫다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서 우선은 DB 락을 먼저 적용해보기로 하고, JMeter도 익힐 겸 v1과 v2를 나눠 진행했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;v1&amp;nbsp;: 별도의 락 없이 기본 로직 구현&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;v2&amp;nbsp;:&amp;nbsp;&lt;b&gt;Pessimistic Lock(DB 락)&lt;/b&gt;을 적용해 중복 참여 방지 및 정합성 확보&lt;/span&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;732&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CmsCL/btsNonzcoO1/FdptvQ8rCYc17Pah7CQE4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CmsCL/btsNonzcoO1/FdptvQ8rCYc17Pah7CQE4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CmsCL/btsNonzcoO1/FdptvQ8rCYc17Pah7CQE4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCmsCL%2FbtsNonzcoO1%2FFdptvQ8rCYc17Pah7CQE4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;958&quot; height=&quot;732&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;732&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;코드 자체는 이벤트를 가져올 때 제외하고는 모두 동일하다.&amp;nbsp;v2에서는 다음과 같이 비관적 락이 적용된&amp;nbsp;findByIdForUpdate()&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;1131&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOfLaJ/btsNn0dqfbM/7O8hBtsfkag4ffXG1zaYA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOfLaJ/btsNn0dqfbM/7O8hBtsfkag4ffXG1zaYA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOfLaJ/btsNn0dqfbM/7O8hBtsfkag4ffXG1zaYA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOfLaJ%2FbtsNn0dqfbM%2F7O8hBtsfkag4ffXG1zaYA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1131&quot; height=&quot;296&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;정원이 10명인 이벤트에 200명이 동시에 이벤트를 신청하다고 가정하고 테스트 해보니, 그 결과는 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNAJi3/btsNn8oK4E3/Kval56vw3fVlQSDJNLnQkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNAJi3/btsNn8oK4E3/Kval56vw3fVlQSDJNLnQkK/img.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;993&quot; data-is-animation=&quot;false&quot; style=&quot;width: 36.6806%; margin-right: 10px;&quot; data-widthpercent=&quot;37.11&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNAJi3/btsNn8oK4E3/Kval56vw3fVlQSDJNLnQkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNAJi3%2FbtsNn8oK4E3%2FKval56vw3fVlQSDJNLnQkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;993&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ifrxa/btsNn9useDN/Je9WCnrCRy80031IUFskHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ifrxa/btsNn9useDN/Je9WCnrCRy80031IUFskHK/img.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;586&quot; data-is-animation=&quot;false&quot; style=&quot;width: 62.1566%;&quot; data-widthpercent=&quot;62.89&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ifrxa/btsNn9useDN/Je9WCnrCRy80031IUFskHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIfrxa%2FbtsNn9useDN%2FJe9WCnrCRy80031IUFskHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;No Lock(왼) / DB Lock(오) &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mFm7p/btsNlb1dLEJ/0fZ1eDhH9yKCDnxlMrjIGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mFm7p/btsNlb1dLEJ/0fZ1eDhH9yKCDnxlMrjIGk/img.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mFm7p/btsNlb1dLEJ/0fZ1eDhH9yKCDnxlMrjIGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmFm7p%2FbtsNlb1dLEJ%2F0fZ1eDhH9yKCDnxlMrjIGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Ct1h/btsNnBrYm4D/b78tThyZNghSCsjFY7jFq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Ct1h/btsNnBrYm4D/b78tThyZNghSCsjFY7jFq1/img.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Ct1h/btsNnBrYm4D/b78tThyZNghSCsjFY7jFq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Ct1h%2FbtsNnBrYm4D%2Fb78tThyZNghSCsjFY7jFq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;No Lock(왼) / DB Lock(오) &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;정리해보면,&amp;nbsp;&lt;b&gt;No Lock 버전은 응답 속도는 빠르지만 초과 신청이 발생해 정합성이 깨졌다.&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;반면,&amp;nbsp;&lt;b&gt;DB Lock을 적용한 버전은 응답 시간과 처리량은 감소했지만, 정확히 10명만 신청되며 정합성을 보장&lt;/b&gt;할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redisson 기반 분산 락(v3) 적용 시에는&amp;nbsp;&lt;b&gt;이 두 가지 요소를 모두 개선하는 방향으로 고도화를 진행&lt;/b&gt;할 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #212529; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;항목&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; No Lock&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; DB Lock &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;요청 수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;200&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;200&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;평균 응답 시간&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;53ms&lt;/b&gt;&amp;nbsp; &lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;415ms&lt;/b&gt;&amp;nbsp; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최대 응답 시간&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;199ms&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;753ms&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;에러율&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;90.50%&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;95.00%&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Throughput&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;507.6/sec&lt;/b&gt;&amp;nbsp; &lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;233.9/sec&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;전송/수신 KB/sec&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;208.22 / 109.06&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;94.20 / 50.26&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;테스트&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;테스트&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;현재는&amp;nbsp;&lt;b&gt;DB 락 기반&lt;/b&gt;으로 이벤트 신청 API의 동시성 문제를 방지하고 있으며, JMeter를 활용해&amp;nbsp;&lt;b&gt;정합성 중심의 테스트를 수행&lt;/b&gt;했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단일 요청 기준의 API는 Postman으로 충분했지만, 이벤트와 같이 여러 사용자가 동시에 접근할 수 있는 기능에 대해서는&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;멀티 스레드 환경에서 예상치 못한 충돌이 없는지 중점적으로 검증&lt;/b&gt;했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이후 Redisson 적용 시, 동일한 시나리오로 성능 및 안정성 테스트를 진행해볼 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;postman&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Postman&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;주요 용도: REST API 기본 기능 테스트 및 빠른 검증&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;인증, 회원가입, 예약, 이벤트 등 API의 정상 응답/에러 응답 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다양한 요청 시나리오를 환경 변수 설정을 통해 반복 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;팀원 간 공유를 위해 테스트 컬렉션 구성 및 문서화&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;jmeter&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;JMeter&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;주요 용도: 이벤트 신청 API 등 동시성 문제 발생 가능 구간의 정합성 테스트, 성능 테스트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;현재는 DB 락(Pessimistic Lock)을 적용한 상태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이벤트 신청 시 중복 참여나 초과 신청이 발생하지 않는지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다양한 스레드 수로 시뮬레이션하며 정합성 유지 여부 체크&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;성능보다 데이터 충돌 방지 및 동시 요청 시 안정성 확인이 목적&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;향후 Redisson 분산 락 전환 전 비교 테스트를 위한 기반 마련&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>백엔드</category>
      <category>프로젝트</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/387</guid>
      <comments>https://3uomlkh.tistory.com/387#entry387comment</comments>
      <pubDate>Thu, 10 Apr 2025 23:49:42 +0900</pubDate>
    </item>
    <item>
      <title> ️ GitHub Actions 기반 CD 실전 트러블슈팅: EC2, ECR, ElastiCache</title>
      <link>https://3uomlkh.tistory.com/386</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;처음으로 자동 배포를 설정하면서 겪은 트러블슈팅을 기록하고자 한다.✍ &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;우선 CI/CD 환경은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2: Ubuntu 22.04, Docker 설치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ECR: Spring Boot 앱 이미지를 빌드하여 push&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RDS: MySQL DB 사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;배포 방식: GitHub Actions를 통한 CI/CD 자동화&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;ec2-배포-시-오류-발생&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2 배포 시 오류 발생&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 id=&quot;command-not-found-에러&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;command not found 에러&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;docker: command not found&amp;nbsp;에러는 EC2 서버에 Docker가 설치되어 있지 않아서 발생한 오류다. GitHub Actions에서 EC2로 SSH 접속은 성공했지만, EC2 안에 docker 명령어 자체가 없어서 전부 실패한 상황이다.&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;870&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfBzrf/btsM3hUojxj/if34vcGzSbRF2NPPYGVHH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfBzrf/btsM3hUojxj/if34vcGzSbRF2NPPYGVHH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfBzrf/btsM3hUojxj/if34vcGzSbRF2NPPYGVHH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfBzrf%2FbtsM3hUojxj%2Fif34vcGzSbRF2NPPYGVHH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;870&quot; height=&quot;327&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2에 SSH 접속 후 아래 명령어 실행해 Docker를 설치한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위와 비슷하게&amp;nbsp;aws: command not found&amp;nbsp;에러도 발생했다. 이 에러는 EC2 서버에 awscli가 아예 설치되어 있지 않아서 발생한 에러로 역시 awscli를 한 번 설치하면 해결된다.&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;1280&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WtA0J/btsM2fJAt5j/mY20kk040fo9734Utt4ti0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WtA0J/btsM2fJAt5j/mY20kk040fo9734Utt4ti0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WtA0J/btsM2fJAt5j/mY20kk040fo9734Utt4ti0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWtA0J%2FbtsM2fJAt5j%2FmY20kk040fo9734Utt4ti0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;469&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2에 SSH 접속 후 아래 명령어 실행한다.&amp;nbsp;apt install awscli&amp;nbsp;명령어를 통해 설치를 하려 했지만, Ubuntu 24.04부터는 AWS에서 제공하는 공식 zip으로 설치해야 한다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;curl &quot;https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip&quot; -o &quot;awscliv2.zip&quot;
unzip awscliv2.zip
sudo ./aws/install&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;만약 unzip도 설치되어있지 않다면 먼저 설치해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install -y unzip&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;설치 후, 다음 명령어로 정상적으로 설치되었는지 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;aws --version&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;  GitHub Actions에서는 왜 aws 명령어가 되는데?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;GitHub Actions에서 aws 명령어가 동작하는 이유는, GitHub의 리눅스 러너에 AWS CLI가&amp;nbsp;&lt;b&gt;기본적으로 설치되어 있기 때문&lt;/b&gt;이다. 반면 EC2는 직접 설정하는 가상 서버이기 때문에, 필요한 도구는 모두&amp;nbsp;&lt;b&gt;수동으로 설치해주어야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;aws-iam-권한-연결-문제&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;AWS IAM 권한 연결 문제&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이제 aws 명령어 실행은 잘 되지만,&amp;nbsp;&lt;i&gt;&lt;span style=&quot;color: #666666;&quot;&gt;Unable to locate credentials. You can configure credentials by running &quot;aws configure&quot;&lt;/span&gt;&lt;/i&gt;.&amp;nbsp;이라는 에러가 발생했다. 이 에러는 EC2에서 AWS 인증정보(Access Key, Secret Key)가 아예 없는 상태여서 발생한 것이다.&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;754&quot; data-origin-height=&quot;517&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhkSm8/btsM2e40AuS/nAukk8rkbTfaVa5jxTScO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhkSm8/btsM2e40AuS/nAukk8rkbTfaVa5jxTScO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhkSm8/btsM2e40AuS/nAukk8rkbTfaVa5jxTScO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhkSm8%2FbtsM2e40AuS%2FnAukk8rkbTfaVa5jxTScO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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-origin-width=&quot;754&quot; data-origin-height=&quot;517&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자세한 원인을 살펴보니...&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2에서&amp;nbsp;aws ecr get-login-password를 쓰려면 AWS IAM 권한이 있어야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2 안에&amp;nbsp;~/.aws/credentials&amp;nbsp;파일이 없어서 발생했다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, EC2가 ECR에 접근할 수 있는 권한이 없다는 뜻이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서 EC2에 IAM Role을 연결해 해결했다. EC2 인스턴스 세부정보를 보니, IAM Role이 비어있는 것을 확인할 수 있다.&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;1280&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgP7ri/btsM2J4XmCc/tHKHhTib98VQl5EtYs4V81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgP7ri/btsM2J4XmCc/tHKHhTib98VQl5EtYs4V81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgP7ri/btsM2J4XmCc/tHKHhTib98VQl5EtYs4V81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgP7ri%2FbtsM2J4XmCc%2FtHKHhTib98VQl5EtYs4V81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;367&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;367&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서 IAM 역할을 EC2로 해서 새로 만든 후,&amp;nbsp;AmazonEC2ContainerRegistryFullAccess&amp;nbsp;권한을 허용했다. ECR은 Private Registry라서 IAM 권한이 없으면 pull에 실패한다.&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;737&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tYCF1/btsM32Je0C2/n75ZbgoW2k5h6ks0ZVE0Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tYCF1/btsM32Je0C2/n75ZbgoW2k5h6ks0ZVE0Ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tYCF1/btsM32Je0C2/n75ZbgoW2k5h6ks0ZVE0Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtYCF1%2FbtsM32Je0C2%2Fn75ZbgoW2k5h6ks0ZVE0Ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;606&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다음과 같이 설정해야 EC2가 ECR에서 pull/push 포함 풀 권한을 가진다.&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;1280&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GrIpa/btsM3FncUpz/evMvwUFZokBprfcfkI26T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GrIpa/btsM3FncUpz/evMvwUFZokBprfcfkI26T0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GrIpa/btsM3FncUpz/evMvwUFZokBprfcfkI26T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGrIpa%2FbtsM3FncUpz%2FevMvwUFZokBprfcfkI26T0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;416&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;docker-pull-실패&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;docker pull 실패&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;1280&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEnR5v/btsM1dMyw7l/39oF9UMhskqPHbB11QqRq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEnR5v/btsM1dMyw7l/39oF9UMhskqPHbB11QqRq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEnR5v/btsM1dMyw7l/39oF9UMhskqPHbB11QqRq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEnR5v%2FbtsM1dMyw7l%2F39oF9UMhskqPHbB11QqRq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;226&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;에러 메시지를 분석해보니 두 가지 문제가 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. docker login 관련 에러&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;erlang&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;Your authorization token has expired.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 메시지는 ECR 인증 토큰이 만료되었음을 의미한다. AWS ECR은 보안을 위해 로그인 토큰이 12시간 동안만 유효하도록 설계되어 있다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;나는 EC2 인스턴스에 이미&amp;nbsp;&lt;span style=&quot;color: #666666;&quot;&gt;AmazonEC2ContainerRegistryFullAccess&amp;nbsp;&lt;/span&gt;IAM 역할을 부여했기 때문에, 별도로 로그인하지 않아도 될 줄 알았다. 하지만 실제로는 그렇지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;✔️ 정리하자면...&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2가 ECR에 접근할 권한(IAM Role)은 있지만, Docker는 AWS IAM을 인식하지 못하기 때문에 별도의 로그인 절차가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서 AWS CLI를 통해 인증 토큰을 받아서 Docker에 전달해줘야 한다. 다음 명령어를&amp;nbsp;Deploy on EC2&amp;nbsp;단계에 추가하자&amp;nbsp;docker pull이 정상적으로 작동했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_URI&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;✔️ IAM 권한과 Docker 인증은 별개!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;IAM Role은&amp;nbsp;&lt;b&gt;&amp;ldquo;리소스를 사용할 수 있는가&amp;rdquo;&lt;/b&gt;를 판단하는 권한이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 Docker는 AWS의 권한 체계를 모름 &amp;rarr; AWS CLI로 토큰을 받아서&amp;nbsp;docker login으로 명시적으로 인증해줘야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또한, ECR 인증 토큰은&amp;nbsp;&lt;b&gt;단기(12시간 유효) 토큰&lt;/b&gt;이기 때문에, 주기적으로 재로그인하는 로직이 필요할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;⚙️ 운영 환경에서는 ECS, Fargate, EKS 등의&amp;nbsp;&lt;b&gt;컨테이너 오케스트레이션 서비스&lt;/b&gt;를 사용하는 경우가 많다. 이런 경우에는 해당 서비스가&amp;nbsp;&lt;b&gt;ECR 인증을 자동으로 처리&lt;/b&gt;해주기 때문에 별도의&amp;nbsp;docker login이 필요 없다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 포트 충돌 에러&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;에러 메시지 중 다음과 같은 내용이 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Bind for 0.0.0.0:8080 failed: port is already allocated.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이는 EC2 내에서 포트 8080을 이미 점유하고 있는 컨테이너가 실행 중이라는 뜻이다. 대부분 이전에 실행된 컨테이너가 종료되지 않아서 발생하는 문제다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서&amp;nbsp;docker run을 하기 전에 기존 컨테이너를 정리해주는 명령어를 추가했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker ps -q --filter ancestor=$ECR_URI | xargs -r docker stop
docker ps -aq --filter ancestor=$ECR_URI | xargs -r docker rm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; $ECR_URI는 ECR 이미지 주소다. 하지만 이전과 다른 태그가 붙은 이미지거나 이름이 바뀌었다면 해당 필터에 안 잡힐 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;더 안전하게 하려면 컨테이너 이름을 명시해서 정리하는 것도 고려할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;docker stop my-app-container || true
docker rm my-app-container || true&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;정상적으로 포트가 비워졌는지 확인하려면 다음 명령어를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;sudo lsof -i :8080&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;health-check-실패&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;health check 실패&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;배포는 성공적으로 완료되었지만, health check는 계속해서 실패하는 문제가 발생했다. 따라서 여러가지 가능성을 체크해보며 문제를 해결했다.&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;623&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdGyf7/btsM3EWbXTz/ywDRqjmkBV1kk8zlA8gb10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdGyf7/btsM3EWbXTz/ywDRqjmkBV1kk8zlA8gb10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdGyf7/btsM3EWbXTz/ywDRqjmkBV1kk8zlA8gb10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdGyf7%2FbtsM3EWbXTz%2FywDRqjmkBV1kk8zlA8gb10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;551&quot; data-origin-width=&quot;623&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;1단계-docker-컨테이너-상태-확인&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1단계: Docker 컨테이너 상태 확인&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다음 명령어들을 통해 실행 상태 및 로그를 확인할 수 있다. 이 단계에서 컨테이너가 제대로 실행되지 않았거나, 실행되자마자 꺼지는 경우엔 로그에서 에러 메시지를 찾아야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;docker ps                   # 현재 실행 중인 컨테이너 목록
docker ps -a                # 실행 이력이 있는 모든 컨테이너
docker logs &amp;lt;컨테이너 ID&amp;gt;  # 죽은 컨테이너의 로그까지 확인&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2단계-spring-actuator-설정-확인&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2단계: Spring Actuator 설정 확인&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;컨테이너가 정상 작동하고 있는 것처럼 보이는데도 health check가 실패했다면,build.gradle에 Spring Actuator 의존성이 제대로 추가되었는지 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-actuator'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또한 배포 시 사용되는&amp;nbsp;application-prod.yml에 다음 설정이 포함되어 있는지 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;management:
  endpoints:
    web:
      exposure:
        include: health,info&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3단계-spring-profile-활성화-확인&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3단계: Spring Profile 활성화 확인&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위 설정들이&amp;nbsp;application-prod.yml에 포함되어 있다면, 해당 프로파일이 실제로 활성화되고 있는지도 반드시 확인해야 한다. Spring Boot 앱을 실행할 때 다음과 같이 prod 프로파일이 활성화되어야 설정이 적용된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;-Dspring.profiles.active=prod&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존에는&amp;nbsp;docker run&amp;nbsp;명령어에서 직접 전달했지만,&amp;nbsp;Dockerfile&amp;nbsp;내&amp;nbsp;ENTRYPOINT를 다음과 같이 수정해 고정하는 방식으로 개선했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ENTRYPOINT [&quot;java&quot;, &quot;-Dspring.profiles.active=prod&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4단계-actuator-접근-권한-확인&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4단계: actuator 접근 권한 확인&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;설정이 잘 되어 있는데도 여전히 health check가 실패한다면,&amp;nbsp;&lt;b&gt;보안 설정(SecurityConfig)&lt;/b&gt;을 점검해야 한다. EC2에 SSH 접속해 다음 명령어를 입력해 확인해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;curl http://localhost:8080/actuator/health&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;해당 명령어를 실행했을 때 아래처럼&amp;nbsp;403 Forbidden&amp;nbsp;응답이 오면, actuator endpoint는 열려 있으나 접근 권한이 막혀 있다는 뜻이다.&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;1095&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zk9Aq/btsM1Kb6UXl/SK7IpUM1Ml2q38syQny0c0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zk9Aq/btsM1Kb6UXl/SK7IpUM1Ml2q38syQny0c0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zk9Aq/btsM1Kb6UXl/SK7IpUM1Ml2q38syQny0c0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzk9Aq%2FbtsM1Kb6UXl%2FSK7IpUM1Ml2q38syQny0c0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1095&quot; height=&quot;301&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또,&amp;nbsp;SecurityConfig&amp;nbsp;클래스에서 actuator 경로도 허용해야 한다.&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;1280&quot; data-origin-height=&quot;549&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pptRp/btsM3QWpynt/Gkn6euPC3DrYEYc71NjR10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pptRp/btsM3QWpynt/Gkn6euPC3DrYEYc71NjR10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pptRp/btsM3QWpynt/Gkn6euPC3DrYEYc71NjR10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpptRp%2FbtsM3QWpynt%2FGkn6euPC3DrYEYc71NjR10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;549&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;549&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;5단계-외부-접속-테스트-및-보안-그룹-설정&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;5단계: 외부 접속 테스트 및 보안 그룹 설정&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2 내부에서는 health 응답이 잘 오는데도 외부 health check가 실패한다면, EC2 보안 그룹 설정을 점검해야 한다. AWS를 사용할 때는 특히나 보안 그룹 설정 문제인 경우가 많으니 잘 확인해두는 것이 좋다. 포트 8080이 퍼블릭으로 열려 있어야 외부에서 접근 가능하다.&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;1280&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVvR0Y/btsM3ZZ2DOk/RHMzJZTMdAJq1HYZ84LsBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVvR0Y/btsM3ZZ2DOk/RHMzJZTMdAJq1HYZ84LsBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVvR0Y/btsM3ZZ2DOk/RHMzJZTMdAJq1HYZ84LsBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVvR0Y%2FbtsM3ZZ2DOk%2FRHMzJZTMdAJq1HYZ84LsBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;468&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2에서 다시 확인해보니 정상적으로 HTTP/1.1 200 OK 응답이 온 것을 볼 수 있다.&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;1280&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csFLT3/btsM14V3ozd/OOtBEDD7uGWxrGOqGnbyLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csFLT3/btsM14V3ozd/OOtBEDD7uGWxrGOqGnbyLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csFLT3/btsM14V3ozd/OOtBEDD7uGWxrGOqGnbyLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsFLT3%2FbtsM14V3ozd%2FOOtBEDD7uGWxrGOqGnbyLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;300&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다음과 같이 웹 브라우저를 통해서도 볼 수 있는데,&amp;nbsp;http://&amp;lt;EC2 퍼블릭 IPv4 주소&amp;gt;:8080/actuator/health&amp;nbsp;와 같이 입력해야 한다. 나는 EC2에 탄력적 IP를 할당하여 고정된 주소로 항상 접근 가능하도록 연결했다.&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;446&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddlPCK/btsM1Nfy79e/XmJzO4IhV9ol1C7CUEstiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddlPCK/btsM1Nfy79e/XmJzO4IhV9ol1C7CUEstiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddlPCK/btsM1Nfy79e/XmJzO4IhV9ol1C7CUEstiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddlPCK%2FbtsM1Nfy79e%2FXmJzO4IhV9ol1C7CUEstiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;446&quot; height=&quot;580&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;redis-연결-실패---elasticache-설정&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis 연결 실패 - ElastiCache 설정&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;배포 테스트가 정상적으로 끝났다고 생각했지만, 이후 테스트 중 Redis 연결 관련 오류가 발생했다.&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;1280&quot; data-origin-height=&quot;223&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfuksm/btsM20ZE9U6/w0xKOE7CwOg4zVFMakygz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfuksm/btsM20ZE9U6/w0xKOE7CwOg4zVFMakygz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfuksm/btsM20ZE9U6/w0xKOE7CwOg4zVFMakygz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfuksm%2FbtsM20ZE9U6%2Fw0xKOE7CwOg4zVFMakygz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;223&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;223&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;로그를 자세히 보면 Redis 관련 오류라는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Caused by: io. lettuce. core. RedisConnectionException: Unable to connect to
eightyage-cache. 2i7efi.ng. 0001.apn2. cache. amazonaws. com/&amp;lt;unresolved&amp;gt;: 6379&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;redis-설정-점검&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis 설정 점검&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;application-prod.yml에는 아래와 같이 Redis 설정을 해두었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;data:
  redis:
    host: ${REDIS_HOST}
    port: 6379&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그리고&amp;nbsp;REDIS_HOST&amp;nbsp;환경 변수는 GitHub Repository Secrets에 정상적으로 등록했다. 이 환경 변수에는 ElastiCache Redis 인스턴스의 엔드포인트가 들어간다.&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;879&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4Vv69/btsM3QPExtb/fQwKtrJ6AU68fWAskan6kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4Vv69/btsM3QPExtb/fQwKtrJ6AU68fWAskan6kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4Vv69/btsM3QPExtb/fQwKtrJ6AU68fWAskan6kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4Vv69%2FbtsM3QPExtb%2FfQwKtrJ6AU68fWAskan6kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;879&quot; height=&quot;700&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;ec2에서-직접-접속-확인&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2에서 직접 접속 확인&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2에서 telnet으로 Redis에 접속을 시도해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;telnet &amp;lt;REDIS_HOST&amp;gt; 6379&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;1280&quot; data-origin-height=&quot;85&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pSdKj/btsM23IQedt/BJyNtgA7AUFm76yYnzuAw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pSdKj/btsM23IQedt/BJyNtgA7AUFm76yYnzuAw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pSdKj/btsM23IQedt/BJyNtgA7AUFm76yYnzuAw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpSdKj%2FbtsM23IQedt%2FBJyNtgA7AUFm76yYnzuAw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;85&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;원인-분석-보안-그룹-인바운드-미설정&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;원인 분석: 보안 그룹 인바운드 미설정&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;해당 문제는 ElastiCache Redis 인스턴스의 보안 그룹이 EC2의 접근을 허용하지 않아서 발생한 것이었다. Redis가 퍼블릭하게 열리지 않기 때문에, EC2처럼 내부 통신을 하려면 보안 그룹 간 인바운드 허용 설정이 필수다. 다음과 같이 인바운드 규칙에 EC2 인스턴스의 보안 그룹을 소스로 추가해준다.&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;1280&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xkL5O/btsM38JhX5a/0A2cwAGbhw3aCikqcS8Rx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xkL5O/btsM38JhX5a/0A2cwAGbhw3aCikqcS8Rx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xkL5O/btsM38JhX5a/0A2cwAGbhw3aCikqcS8Rx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxkL5O%2FbtsM38JhX5a%2F0A2cwAGbhw3aCikqcS8Rx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;784&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;  보안 그룹 간 참조를 사용하는 이유: EC2 인스턴스의 IP는 변할 수 있으므로, 고정된 IP가 아닌 EC2가 속한 보안 그룹 자체를 참조하는 방식이 유지보수에 유리하다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;redis-연결-성공&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Redis 연결 성공&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;1280&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsr7im/btsM20yAL5t/twOBkvloPePBMgFFfsUL41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsr7im/btsM20yAL5t/twOBkvloPePBMgFFfsUL41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsr7im/btsM20yAL5t/twOBkvloPePBMgFFfsUL41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbsr7im%2FbtsM20yAL5t%2FtwOBkvloPePBMgFFfsUL41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;86&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring Boot Actuator에서도 Redis 관련 항목이 포함된 health 정보가 정상적으로 출력되었다.&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;441&quot; data-origin-height=&quot;863&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XrHGq/btsM100qFS1/EWI5jHfsvmjjQgvzjMwQJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XrHGq/btsM100qFS1/EWI5jHfsvmjjQgvzjMwQJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XrHGq/btsM100qFS1/EWI5jHfsvmjjQgvzjMwQJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXrHGq%2FbtsM100qFS1%2FEWI5jHfsvmjjQgvzjMwQJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;350&quot; height=&quot;685&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;863&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>AWS</category>
      <category>CICD</category>
      <category>EC2</category>
      <category>ECR</category>
      <category>ElastiCache</category>
      <category>GithubActions</category>
      <category>RDS</category>
      <category>S3</category>
      <category>트러블슈팅</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/386</guid>
      <comments>https://3uomlkh.tistory.com/386#entry386comment</comments>
      <pubDate>Mon, 31 Mar 2025 16:15:12 +0900</pubDate>
    </item>
    <item>
      <title>QueryDSL 사용하기(with JPA)</title>
      <link>https://3uomlkh.tistory.com/383</link>
      <description>&lt;h1 id=&quot;querydsl란&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;QueryDSL란?&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;DSL(Domain-Specific-Language)&lt;/b&gt;&amp;nbsp;: 특정 도메인에서 발생하는 문제를 효과적으로 해결하기 위해 설계된 언어.(ex. SQL, CSS, Regex 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;QueryDSL&lt;/b&gt;&amp;nbsp;: SQL 형식의 쿼리를 Type-Safe하게 생성할 수 있도록 하는 DSL을 제공하는 라이브러리. 엔티티의 매핑정보를 활용하여 쿼리에 적합하도록 쿼리 전용 클래스(Q클래스)로 재구성해준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ Q클래스란?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Q클래스란 엔티티 클래스 속성과 구조를 설명해주는 메타데이터로, Type-Safe하게 쿼리 조건을 설정할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;annotationProcessor를 통해 생성된 실제 Q클래스는 다음과 같다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다 캡쳐하진 못했지만, Todo의 필드가 QTodo에서도 보이는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1OYJT/btsMT4AqyvH/cWG1yckljhuWRMfgq0czG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1OYJT/btsMT4AqyvH/cWG1yckljhuWRMfgq0czG0/img.png&quot; data-origin-width=&quot;891&quot; data-origin-height=&quot;758&quot; data-is-animation=&quot;false&quot; style=&quot;width: 44.0004%; margin-right: 10px;&quot; data-widthpercent=&quot;44.52&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1OYJT/btsMT4AqyvH/cWG1yckljhuWRMfgq0czG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1OYJT%2FbtsMT4AqyvH%2FcWG1yckljhuWRMfgq0czG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;891&quot; height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pexrk/btsMSnoaIca/quJwRhwCugfCaxKJ0NTj70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pexrk/btsMSnoaIca/quJwRhwCugfCaxKJ0NTj70/img.png&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;856&quot; data-is-animation=&quot;false&quot; style=&quot;width: 54.8368%;&quot; data-widthpercent=&quot;55.48&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pexrk/btsMSnoaIca/quJwRhwCugfCaxKJ0NTj70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpexrk%2FbtsMSnoaIca%2FquJwRhwCugfCaxKJ0NTj70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1254&quot; height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Todo 엔티티(왼) / QTodo(오) &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 id=&quot;querydsl-시작하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;QueryDSL 시작하기&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;h2 id=&quot;querydsl-jpa-의존성-추가하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;querydsl-jpa 의존성 추가하기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Boot 3 이후부터는 javax에서 jakarta로 변경되었기 때문에 뒤에 꼭&amp;nbsp;&lt;b&gt;jakarta&lt;/b&gt;를 붙여야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Q클래스를 사용하기 위해&amp;nbsp;&lt;b&gt;annotationProcessor&lt;/b&gt;도 추가한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;clean&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor &quot;com.querydsl:querydsl-apt:5.0.0:jakarta&quot;
annotationProcessor &quot;jakarta.annotation:jakarta.annotation-api&quot;
annotationProcessor &quot;jakarta.persistence:jakarta.persistence-api&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같은 gradle script를 추가해 협업 시 Q클래스의 위치를 지정해줄 수 있디.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;def generated = 'src/main/generated'

tasks.withType(JavaCompile).configureEach {
    options.getGeneratedSourceOutputDirectory().set(file(generated))
}

clean {
    delete file(generated)
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여러 개발자가 같은 Q클래스를 Git에 올리면, 빌드할 때마다 변경된다. 따라서&amp;nbsp;.gitignore에 다음과 같이 등록해 놓는다. QueryDSL의 라이브러리 버전에 따라 생성되는 Q클래스 생김새가 달라질 수 있어 추가 해놓는 것이 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;clean&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;###QueryDSL###
/src/main/generated/&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;jpaqueryfactory-bean-등록&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JPAQueryFactory Bean 등록&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JPAQueryFactory를 사용하려면&amp;nbsp;&lt;b&gt;JPAConfiguration 설정 클래스&lt;/b&gt;를 추가해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;QueryDSL이 JPA를 통해 엔티티를 조회하기 때문에&amp;nbsp;&lt;b&gt;EntityManager&lt;/b&gt;가 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JPAQueryFactory 생성 시 EntityManager를 넣어준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Configuration
public class QueryDslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;todorepositoryquery-interface-생성&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;TodoRepositoryQuery interface 생성&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인터페이스를 하나 만들고, QueryDSL로 수행항 메서드를 선언한다. 나는 TodoRepositoryQuery라는 이름으로 만들었다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;public interface TodoRepositoryQuery {

    Optional&amp;lt;Todo&amp;gt; findByIdWithUser(Long todoId);

}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;todorepositoryqueryimpl-생성&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;TodoRepositoryQueryImpl 생성&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위에서 만든 TodoRepositoryQuery 인터페이스를 구현하는 TodoRepositoryQueryImpl 클래스를 만든다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;클래스 이름은 뒤에&amp;nbsp;&lt;b&gt;Impl이라는 postfix를 반드시 붙여 구현&lt;/b&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class TodoRepositoryQueryImpl implements TodoRepositoryQuery {

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public Optional&amp;lt;Todo&amp;gt; findByIdWithUser(Long todoId) {
        return Optional.ofNullable(jpaQueryFactory.selectFrom(todo)
                .where(todo.id.eq(todoId))
                .leftJoin(todo.user)
                .fetchJoin() 
                .fetchOne());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;사용할-jpa-repository-수정&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;사용할 JPA Repository 수정&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위에서 만든 TodoRepositoryQuery를 실제로 사용할 TodoRepository에 extends 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;public interface TodoRepository extends JpaRepository&amp;lt;Todo, Long&amp;gt;, TodoRepositoryQuery {
	//...중략
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;querydsl-쿼리-작성하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;QueryDSL 쿼리 작성하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;영상 자료에 있는 코드를 다 가져오진 않고 내가 현재 작성 중인 쿼리로 대체하겠다. 검색 조건은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;유저와 함께 일정 검색&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;검색 키워드로 일정 제목 검색&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;일정 생성일 범위로 일정 검색(최신순으로 정렬)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;담당자 닉네임으로 일정 검색&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;검색 결과는 페이징 처리 하기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;일정 제목, 담당자 수, 댓글 개수 보여주기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;처음에는&amp;nbsp;findByTitle()처럼 검색 조건을 따로따로 받아 만드려고 하다가, 검색 API를 하나로 통일하고 파라미터를 추가해 동적 쿼리를 만드는 것이 나은 것 같아 바꿨다. 한편으로는 실무에서 지금보다 검색 조건이 훨씬 많은 경우에도 계속 파라미터만 추가하는 방식으로 구현할까?  라는 궁금증이 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;전체-코드&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;전체 코드&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;응답 DTO로&amp;nbsp;TodoSearchResponse를 새로 만들어 반환하도록 했다. 해당 DTO에는 제목, 담당자 수, 댓글 개수를 담는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@GetMapping(&quot;/todos/search&quot;)
public ResponseEntity&amp;lt;Page&amp;lt;TodoSearchResponse&amp;gt;&amp;gt; searchTodos(
        @AuthenticationPrincipal AuthUser authUser,
        @RequestParam(required = false) String title,
        @RequestParam(required = false) LocalDate startDate,
        @RequestParam(required = false) LocalDate endDate,
        @RequestParam(required = false) String nickName,
        @RequestParam(defaultValue = &quot;1&quot;) int page,
        @RequestParam(defaultValue = &quot;10&quot;) int size) {
    return ResponseEntity.ok(todoService.searchTodos(authUser, title, startDate, endDate, nickName, page, size));
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
public Page&amp;lt;TodoSearchResponse&amp;gt; searchTodos(
        String title, LocalDate startDate, LocalDate endDate, 
        String nickName, Pageable pageable) {

    BooleanBuilder builder = new BooleanBuilder();

    if (title != null &amp;amp;&amp;amp; !title.isEmpty()) {
        builder.and(todo.title.containsIgnoreCase(title));
    }

    if (startDate != null &amp;amp;&amp;amp; endDate != null) {
        builder.and(todo.createdAt.between(startDate.atStartOfDay(), endDate.atTime(23, 59, 59)));
    }

    if (nickName != null &amp;amp;&amp;amp; !nickName.isEmpty()) {
        builder.and(todo.user.nickName.containsIgnoreCase(nickName));
    }

    List&amp;lt;TodoSearchResponse&amp;gt; results = jpaQueryFactory
            .select(Projections.constructor(
                    TodoSearchResponse.class,
                    todo.title,
                    todo.managerCount,
                    todo.commentCount
            ))
            .from(todo)
            .where(builder)
            .orderBy(todo.createdAt.desc())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

    JPAQuery&amp;lt;Long&amp;gt; countQuery = jpaQueryFactory
            .select(todo.count())
            .from(todo)
            .where(builder);

    return PageableExecutionUtils.getPage(results, pageable, countQuery::fetchOne);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;유저와-함께-일정-검색&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;유저와 함께 일정 검색&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;유저와 함께 일정을 검색하는 기능은 기존 쿼리를 QueryDSL로 바꿔야 했다. 기존 JQPL 쿼리는 다음과 같다.&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;1044&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DKCDU/btsMTKa8CmA/KlJkb9NqSl9E45wKIXCbE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DKCDU/btsMTKa8CmA/KlJkb9NqSl9E45wKIXCbE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DKCDU/btsMTKa8CmA/KlJkb9NqSl9E45wKIXCbE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDKCDU%2FbtsMTKa8CmA%2FKlJkb9NqSl9E45wKIXCbE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1044&quot; height=&quot;206&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음은 QueryDSL로 바꾼 코드다. SQL에 익숙하다면 읽기에 문제가 없을 것이다. 참고로,&amp;nbsp;todo와&amp;nbsp;user는 Q클래스에서 import 해온 것이다.&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;1029&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvAGn6/btsMUlu9f69/m03SK6cUTuJEW1Kz7H26Lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvAGn6/btsMUlu9f69/m03SK6cUTuJEW1Kz7H26Lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvAGn6/btsMUlu9f69/m03SK6cUTuJEW1Kz7H26Lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvAGn6%2FbtsMUlu9f69%2Fm03SK6cUTuJEW1Kz7H26Lk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1029&quot; height=&quot;110&quot; data-origin-width=&quot;1029&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1037&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kIs7I/btsMSvlZhhe/5VqMHeJNcczipysSPaflek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kIs7I/btsMSvlZhhe/5VqMHeJNcczipysSPaflek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kIs7I/btsMSvlZhhe/5VqMHeJNcczipysSPaflek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkIs7I%2FbtsMSvlZhhe%2F5VqMHeJNcczipysSPaflek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1037&quot; height=&quot;386&quot; data-origin-width=&quot;1037&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;selectFrom(todo)&amp;nbsp;: Todo 엔티티를 기준으로 조회한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;.leftJoin(todo.user, user)&amp;nbsp;: 일정에 해당하는 User 정보를 함께 조회한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;.fetchJoin()&amp;nbsp;: N+1 문제를 해결할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;.fetchOne()&amp;nbsp;: 단일 엔티티를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️&amp;nbsp;fetchOne()&amp;nbsp;vs&amp;nbsp;fetch()&amp;nbsp;vs&amp;nbsp;fetchFirst()&amp;nbsp;차이&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;fetchOne()&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;쿼리 결과 1개 - 해당 엔티티 반환.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;쿼리 결과 없음 - null 반환.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;쿼리 결과 여러 개 - 예외(NonUniqueResultException) 발생.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;fetch()&amp;nbsp;: 리스트 형태와 같이 여러 개의 결과를 가져오고 싶을 때 사용.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;fetchFirst()&amp;nbsp;:&amp;nbsp;limit(1).fetchOne()과 같은 결과를 낼 때 사용.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;쿼리 결과 1개 - 해당 엔티티 반환.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;쿼리 결과 없음 - null 반환.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;쿼리 결과가 여러 개 - 예외를 발생시키지 않고 첫 번째 결과만 반환.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;fetchOne()은 결과가 유일할 때 사용하고,&amp;nbsp;fetchFirst()는 여러 개 중 하나만 가져올 때 사용.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;검색-키워드로-일정-제목-검색&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;검색 키워드로 일정 제목 검색&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;BooleanBuilder&lt;/b&gt;를 사용해 여러 개의 조건을 동적으로 추가했다. 따라서 제목과 생성일을 동시에 필터링해 검색하거나 닉네임과 생성일을 기준으로 검색하는 등의 동작이 가능하다. BooleanBuilder에 대한 자세한 내용은 아래에서 다룬다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;우선 BooleanBuilder를 먼저 생성하고, 제목이 비어있지 않은 경우에만 builder에&amp;nbsp;and()&amp;nbsp;연산자를 사용해 AND 조건을 추가한다. 따라서 모든 파라미터가 들어온다면 제목, 생성일, 닉네임을 모두 충족하는 일정을 검색하게 된다.&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;622&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9sYSx/btsMT4NW0yV/6DqadKboJhO1ppcC5xbPm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9sYSx/btsMT4NW0yV/6DqadKboJhO1ppcC5xbPm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9sYSx/btsMT4NW0yV/6DqadKboJhO1ppcC5xbPm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9sYSx%2FbtsMT4NW0yV%2F6DqadKboJhO1ppcC5xbPm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;622&quot; height=&quot;172&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;172&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;containsIgnoreCase()&amp;nbsp;: 대소문자 상관없이 특정 문자열을 포함하고 있으면 treu를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;일정-생성일-범위로-일정-검색&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;일정 생성일 범위로 일정 검색&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;createdAT 필드가 startDate와 endDate 사이에 포함되는 데이터를 필터링한다.&amp;nbsp;/todos/search?startDate=2025-03-10&amp;amp;endDate=2025-03-12&amp;amp;page&amp;amp;size&amp;nbsp;와 같이 호출할 경우, createdAt이 2025-03-10 00:00:00 이상이고 2025-03-12 23:59:59 이하인 데이터를 조회한다.&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;1257&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b17l5G/btsMTfinEqU/Gsg4yjg5KzSY80Nkpeb8v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b17l5G/btsMTfinEqU/Gsg4yjg5KzSY80Nkpeb8v0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b17l5G/btsMTfinEqU/Gsg4yjg5KzSY80Nkpeb8v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb17l5G%2FbtsMTfinEqU%2FGsg4yjg5KzSY80Nkpeb8v0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1257&quot; height=&quot;110&quot; data-origin-width=&quot;1257&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;.atStartOfDay()&amp;nbsp;: 해당 날짜의 00:00:00(자정)으로 변환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;.atTime(23, 59, 59)&amp;nbsp;: 해당 날짜의 23:59:59 (하루의 마지막 순간)으로 변환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;담당자-닉네임으로-일정-검색&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;담당자 닉네임으로 일정 검색&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;740&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKzVns/btsMS8jgvqn/MnktAQXSZlwjXkDvBr5sTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKzVns/btsMS8jgvqn/MnktAQXSZlwjXkDvBr5sTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKzVns/btsMS8jgvqn/MnktAQXSZlwjXkDvBr5sTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKzVns%2FbtsMS8jgvqn%2FMnktAQXSZlwjXkDvBr5sTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;740&quot; height=&quot;104&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;공통-코드---projections-사용&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;공통 코드 - Projections 사용&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;635&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMmMpc/btsMUxhXVZM/zjXcJnZshpbtQFArrw2fYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMmMpc/btsMUxhXVZM/zjXcJnZshpbtQFArrw2fYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMmMpc/btsMUxhXVZM/zjXcJnZshpbtQFArrw2fYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMmMpc%2FbtsMUxhXVZM%2FzjXcJnZshpbtQFArrw2fYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;903&quot; height=&quot;635&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;635&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;.select(Projections.constructor(TodoTitleResponse.class, todo.title))&amp;nbsp;: 여기서&amp;nbsp;Projections.constructor()은 DTO로 데이터 매핑 시 사용하는 QueryDSL의 기능이다.&amp;nbsp;TodoTitleResponse&amp;nbsp;DTO를 활용해&amp;nbsp;todo.title&amp;nbsp;값을 매핑하고 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;.where(builder)&amp;nbsp;: 동적 검색 조건을 적용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;.offset(pageable.getOffset())&amp;nbsp;: 조회할 데이터의 시작 위치(몇 개를 건너뛸지)를 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;.limit(pageable.getPageSize())&amp;nbsp;: 가져올 최대 개수를&amp;nbsp;pageable.getPageSize()로 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;fetch()&amp;nbsp;: 리스트 형태로 조회한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;PageableExecutionUtils.getPage()&amp;nbsp;: results.size() &amp;lt; pageable.getPageSize()라면 countQuery 실행 없이 페이징 정보를 생성한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위 코드에서 countQuery는 페이징 시 필요한 정보인 전체 검색 결과 개수를 구하는 역할을 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ PageableExecutionUtils란?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;PageableExecutionUtils&lt;/b&gt;란&amp;nbsp;&lt;b&gt;Spring Data JPA에서 Page 객체를 최적화하여 반환하는 유틸리티 메서드&lt;/b&gt;다. 불필요한 카운트 쿼리를 줄여 성능을 향상시킬 수 있다. PageableExecutionUtils 대신 PageImpl을 사용할 수도 있는데, PageImpl은 데이터를 조회하는 쿼리와 전체 개수를 조회하는 쿼리를 모두 호출해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위 코드를 PageImpl로 바꿔보면 다음과 같다. 항상 countQuery를 실행하므로 불필요한 쿼리를 실행할 가능성이 높다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;long total = countQuery.fetchOne();
return new PageImpl&amp;lt;&amp;gt;(titleList, pageable, total);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;PageableExecutionUtils은 countQuery 실행을 지연시켜 꼭 필요할 때만 실행되도록 최적화하는 방식이다. 데이터의 개수가 적을 경우 countQuery 실행을 생략해 성능을 향상시킨다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;return PageableExecutionUtils.getPage(titleList, pageable, countQuery::fetchOne);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;countQuery를 생략 가능한 경우는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;마지막 페이지 일 때&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;결과&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;결과&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHezRG/btsMUlhDSJc/mrdwfFmvoPqK3SE0PJ5OG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHezRG/btsMUlhDSJc/mrdwfFmvoPqK3SE0PJ5OG1/img.png&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;796&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5055%; margin-right: 10px;&quot; data-widthpercent=&quot;33.28&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHezRG/btsMUlhDSJc/mrdwfFmvoPqK3SE0PJ5OG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHezRG%2FbtsMUlhDSJc%2FmrdwfFmvoPqK3SE0PJ5OG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;957&quot; height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcAEGg/btsMR8LiY2s/dCbvPK5bo0Kr1bdZt5f6pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcAEGg/btsMR8LiY2s/dCbvPK5bo0Kr1bdZt5f6pk/img.png&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;791&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.6768%; margin-right: 10px;&quot; data-widthpercent=&quot;33.45&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcAEGg/btsMR8LiY2s/dCbvPK5bo0Kr1bdZt5f6pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcAEGg%2FbtsMR8LiY2s%2FdCbvPK5bo0Kr1bdZt5f6pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;956&quot; height=&quot;791&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgfcFd/btsMTNeCrSx/kibbYdpSULcwhlQ3KEocoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgfcFd/btsMTNeCrSx/kibbYdpSULcwhlQ3KEocoK/img.png&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;793&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.4921%;&quot; data-widthpercent=&quot;33.27&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgfcFd/btsMTNeCrSx/kibbYdpSULcwhlQ3KEocoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgfcFd%2FbtsMTNeCrSx%2FkibbYdpSULcwhlQ3KEocoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;953&quot; height=&quot;793&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; 제목으로 검색 / 생성일 범위로 검색 / 닉네임으로 검색 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bW3XTO/btsMSRoKtjy/f6x6nVGHYLEGxtn39MnHH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bW3XTO/btsMSRoKtjy/f6x6nVGHYLEGxtn39MnHH1/img.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;801&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.2719%; margin-right: 10px;&quot; data-widthpercent=&quot;33.04&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bW3XTO/btsMSRoKtjy/f6x6nVGHYLEGxtn39MnHH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbW3XTO%2FbtsMSRoKtjy%2Ff6x6nVGHYLEGxtn39MnHH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;954&quot; height=&quot;801&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eSDy4W/btsMTZ0jbxQ/fSm3C70CJfhl2RyodJkMl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eSDy4W/btsMTZ0jbxQ/fSm3C70CJfhl2RyodJkMl0/img.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;787&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.846%; margin-right: 10px;&quot; data-widthpercent=&quot;33.63&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eSDy4W/btsMTZ0jbxQ/fSm3C70CJfhl2RyodJkMl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeSDy4W%2FbtsMTZ0jbxQ%2FfSm3C70CJfhl2RyodJkMl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;954&quot; height=&quot;787&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd1ezK/btsMSTNrwbN/cm8elmtYnKL4CF4V64Q1jK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd1ezK/btsMSTNrwbN/cm8elmtYnKL4CF4V64Q1jK/img.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;794&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5565%;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd1ezK/btsMSTNrwbN/cm8elmtYnKL4CF4V64Q1jK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd1ezK%2FbtsMSTNrwbN%2Fcm8elmtYnKL4CF4V64Q1jK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;954&quot; height=&quot;794&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; 생성일 + 닉네임 검색 / 생성일 + 제목 검색 / 제목 + 닉네임 검색 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;동적-쿼리&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;동적 쿼리&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 id=&quot;booleanexpression&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;BooleanExpression&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;동적 쿼리는 사용자의 요구에 따라 변하는 쿼리를 말한다. 동적 쿼리를 알기위해서는 먼저 BooleanExpression에 대해 알아야 한다. 아래 내용은 우테톡 영상으로 보고 간단하게 정리한 내용이다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;BooleanExpression&lt;/b&gt;&amp;nbsp;: 조건 부분에 들어가는 참/거짓 표현식. 아래와 같은 식을 말한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;.from(store)
.where(store.name.eq(&quot;덮밥&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;BooleanExpression은 여러 개를 조합해 하나의 큰 BooleanExpression을 만들 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여러 메서드로 분리해 작성할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;.where(expression1, expression2)에서&amp;nbsp;, 는&amp;nbsp;AND와 같은 의미를 지닌다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;.from(store)
.where(expression1, expression2)
   
private BooleanExpression expression1(final String name) {
     return null이 아닌 expression;
}
   
private BooleanExpression expression2(final String name) {
     return null이 아닌 expression;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;null이 반환되면 자동으로 조건에서 무시&lt;/b&gt;한다. 만약 아래 코드에서 expression1에 null이 들어가게 된다면 무시하고 expression2에 대해서만 조건을 걸고 쿼리를 실행한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;.from(store)
.where(expression1, expression2)
   
private BooleanExpression expression1(final String name) {
    if (name == null) return null;
     return null이 아닌 expression;
}
   
private BooleanExpression expression2(final String name) {
     return null이 아닌 expression;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 BooleanExpression을 활용해 다음과 같이 동적 쿼리를 작성할 수 있다. BooleanExpression(혹은 null)을 반환하는 matchesCondition 메서드를 having절에 넣어줌으로써 동적으로 having 조건을 설정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public List&amp;lt;Store&amp;gt; findPopularStoreByTotalRateOrOrderWithMin(final String type, final String min) {
    return jpaQueryFactory.selectFrom(store)
            .leftJoin(store.orders, order)
            .leftJoin(order.review, review)
            .groupBy(store.id)
            .having(matchesCondition(type, min))
            .fetch();
}

private BooleanExpression matchesCondition(final String type, final String min) {
    if (type.equals(&quot;orderCount&quot;)) {
        return filterByOrderCount(min);
    }
    if (type.equals(&quot;rate&quot;)) {
        return filterByRate(min);
    }
    return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;booleanbuilder&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;BooleanBuilder&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;BooleanBuilder&lt;/b&gt;는 동적 쿼리를 작성할 때 유용하게 사용되는 클래스다. &lt;b&gt;검색 조건이 유동적으로 바뀌거나 여러 개의 조건을 조합&lt;/b&gt;해야 할 때 주로 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여러 개의 조건을 필요할 때만 추가 가능 &amp;rarr; 동적 쿼리 구현&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;조건이 있으면 추가하고, 없으면 제외하는 유연한 방식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;where() 절에 그대로 넣어서 사용 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;and(), or(), andNot() 등의 메서드 제공&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 위에 작성한 검색 기능에 적합하다고 판단되어 BooleanBuilder를 사용하였다. BooleanExpression 예시 코드처럼 type에 따라 하나의 조건만 적용된다면 BooleanExpression이 더 적합할 수 있겠지만, 여러 개의 조건을 조합할 때는 BooleanBuilder가 더 좋지 않을까 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;booleanexpression과-booleanbuilder-차이&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;BooleanExpression과 BooleanBuilder 차이&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그래서 BooleanExpression과 BooleanBuilder 차이를 한마디로 정리하면 뭘까? &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;768&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FQNHB/btsMSPYNrkC/s5Q4yVPHs9P1y0XdDYvU8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FQNHB/btsMSPYNrkC/s5Q4yVPHs9P1y0XdDYvU8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FQNHB/btsMSPYNrkC/s5Q4yVPHs9P1y0XdDYvU8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFQNHB%2FbtsMSPYNrkC%2Fs5Q4yVPHs9P1y0XdDYvU8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;628&quot; height=&quot;188&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;querydsl에서-projections를-사용하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;QueryDSL에서 Projections를 사용하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;QueryDSL의 Projections는 DTO로 원하는 필드만 선택하여 조회할 때 사용된다. 엔티티를 그대로 조회하면 불필요한 필드까지 가져오게 되지만, Projections를 활용하면 필요한 데이터만 가져올 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;projectionsconstructor--생성자-기반-매핑&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Projections.constructor() : 생성자 기반 매핑&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Projections.constructor()은 DTO의 생성자를 활용해 필드를 매핑하는 방식이다.위에서 검색 기능을 구현할 때 사용한 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;TodoSearchResponse&amp;gt; results = jpaQueryFactory
        .select(Projections.constructor(
                TodoSearchResponse.class,
                todo.title,
                todo.managerCount,
                todo.commentCount
        ))
        .from(todo)
        .fetch();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;projectionsbean--setter-기반-매핑&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Projections.bean() : Setter 기반 매핑&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Projections.bean()은 Setter를 활용해 필드를 매핑하는 방식이다. 필드명이 일치하면 자동 매핑된다. Setter를 활용한 매핑이라 불변 객체에는 적합하지 않다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;.select(Projections.bean(TodoTitleResponse.class, todo.title.as(&quot;title&quot;)))&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;필드명이 다를 경우&amp;nbsp;Expressions.as()를 사용해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;projectionsfields--필드명-자동-매핑&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Projections.fields() : 필드명 자동 매핑&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Projections.fields()는 DTO의 필드명과 일치하는 값이 자동 매핑된다. 기본 생성자가 있어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;TodoSearchResponse&amp;gt; results = jpaQueryFactory
        .select(Projections.fields(
                TodoSearchResponse.class,
                todo.title,
                todo.managerCount,
                todo.commentCount
        ))
        .from(todo)
        .fetch();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;필드명이 다를 경우&amp;nbsp;Expressions.as()를 사용해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;queryprojection&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;@QueryProjection&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DTO 생성자에&amp;nbsp;@QueryProjection을 붙이면 QueryDSL이 자동으로 해당 DTO의 Q클래스를 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
public class TodoTitleResponse {

    private String title;

    @QueryProjection // QueryDSL에서 생성자 기반 매핑을 지원하도록 설정
    public TodoTitleResponse(String title) {
        this.title = title;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ @QueryProjection 장단점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;컴파일 시점에 타입 체크를 할 수 있어 안정적이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DTO 필드 변경 시 쿼리에서 자동으로 감지할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DTO가 QueryDSL에 종속된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;QDTO 클래스를 빌드할 때마다 생성해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;QueryDSL을 제거할 경우 DTO 수정이 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;  참고 자료&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://querydsl.com/&quot;&gt;QueryDSL 공식 문서&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.youtube.com/watch?v=Dz-46mPfkGo&amp;amp;t=508s&quot;&gt;[10분 테코톡] 바론, 블랙캣의 Querydsl with JPA&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://jforj.tistory.com/427#google_vignette&quot;&gt;[SpringBoot] Querydsl에서 페이징 처리하기&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://green-bin.tistory.com/102&quot;&gt;Querydsl 프로젝션으로 DTO 조회&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/383</guid>
      <comments>https://3uomlkh.tistory.com/383#entry383comment</comments>
      <pubDate>Sat, 22 Mar 2025 00:15:35 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] EC2와 RDS를 연결해보자 (+Elastic IP, health check, security groups)</title>
      <link>https://3uomlkh.tistory.com/382</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이번 개인 과제 중 AWS를 실습하는 부분이 있는데, 요구사항은 다음과 같다. 먼저 EC2와 RDS를 구축하고 연결하는 과정을 기록해보려고 한다.✍ &lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;✔️ EC2
- EC2 인스턴스에서 어플리케이션을 실행한다.
- 탄력적 IP를 설정해 외부에서도 접속할 수 있도록 한다.
- 서버 접속 및 Live 상태를 확인할 수 있는 health check API를 만든다.

✔️ RDS
- RDS 데이터베이스를 구축한다.
- EC2에서 실행되는 어플리케이션에 연결한다.

✔️ S3
- S3 버킷을 생성한다.
- 유저의 프로필 이미지 업로드 및 관리 API를 구현한다.&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&quot;프로젝트를-ec2에-배포하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;프로젝트를 EC2에 배포하기&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;h2 id=&quot;로컬에서-프로젝트-빌드하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;로컬에서 프로젝트 빌드하기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인텔리제이에서 프로젝트를 빌드해보자! EC2 인스턴스는 이미 만들어놨다는 가정 하에 진행하겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1) Gradle 탭에서&amp;nbsp;Task - build에서 왼쪽에 톱니바퀴 모양이 있는 build를 두 번 클릭한다.&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;366&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGCvXj/btsMSRvyKlG/ogbcOZk61zEUPr501ZxTnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGCvXj/btsMSRvyKlG/ogbcOZk61zEUPr501ZxTnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGCvXj/btsMSRvyKlG/ogbcOZk61zEUPr501ZxTnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGCvXj%2FbtsMSRvyKlG%2FogbcOZk61zEUPr501ZxTnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;366&quot; height=&quot;636&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;2)&amp;nbsp;BUILD SUCCESSFUL&amp;nbsp;메시지가 뜨고 빌드가 정상적으로 수행되었다면 프로젝트에서&amp;nbsp;build - libs에 생긴 파일을 확인한다.&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;1280&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chPIUW/btsMTN6LeSk/c7ZEv6993tVNo1oWuNgF1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chPIUW/btsMTN6LeSk/c7ZEv6993tVNo1oWuNgF1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chPIUW/btsMTN6LeSk/c7ZEv6993tVNo1oWuNgF1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchPIUW%2FbtsMTN6LeSk%2Fc7ZEv6993tVNo1oWuNgF1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;378&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여기서&amp;nbsp;expert-0.0.1-SNAPSHOT.jar&amp;nbsp;파일을 EC2로 보내야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;493&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLHGnG/btsMUuFxAla/dftp3pKSjkSckVbrCzJjjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLHGnG/btsMUuFxAla/dftp3pKSjkSckVbrCzJjjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLHGnG/btsMUuFxAla/dftp3pKSjkSckVbrCzJjjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLHGnG%2FbtsMUuFxAla%2Fdftp3pKSjkSckVbrCzJjjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;329&quot; data-origin-width=&quot;493&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; &amp;nbsp;.jar&amp;nbsp;파일이 뭐길래...?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Java ARchive (자바 아카이브) 파일의 줄임말.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Spring Boot 프로젝트 전체를 하나로 압축한 실행 파일&lt;/b&gt;이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;내가 작성한 소스 코드, 사용하는 라이브러리, 설정 파일, 실행 메타 정보 등이 전부 들어있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 이 파일만 보내면 EC2에서도 별다른 설치 없이 바로 실행할 수 있다!&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;⚠️ 빌드 과정에서 발생한 오류&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Error creating bean with name 'jwtAuthenticationFilter' defined in file ...&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JwtAuthenticationFilter&amp;nbsp;클래스가&amp;nbsp;JwtUtil을 주입받으려고 했는데 스프링 컨테이너에&amp;nbsp;JwtUtil&amp;nbsp;빈이 등록되어 있지 않아 발생한 오류이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JwtUtil을 Mock으로 주입해 해결했다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Security 인증이 걸리는 테스트가 있어 인증 객체가 필요하다는 문제가 있었다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;@WithMockUser을 추가해 테스트용으로 가짜 인증 객체(SecurityContext)에 넣어 해결했다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;ec2에-jar-파일-보내고-실행하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;EC2에 .jar 파일 보내고 실행하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1) 로컬에서&amp;nbsp;.jar&amp;nbsp;파일을 EC2로 보낸다. 로컬에서 다음 명령어를 입력한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;나는 window라서 wsl에서 입력했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;scp -i key.pem build/libs/expert-0.0.1-SNAPSHOT.jar ubuntu@EC2_IP:/home/ubuntu/&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;혹시&amp;nbsp;key.pem과 빌드된 파일이 다른 곳에 있다면 각 경로를 입력해줘야 한다. 나는 각자 다른 곳에 있어서 다음과 같이 경로를 하나하나 입력했다. 만약 잘 전송됐다면 왼쪽에 파일명이 뜨고 오른쪽에 100% 라고 뜰 것이다.&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;1280&quot; data-origin-height=&quot;69&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vZUxS/btsMT6EUTMo/RTJP8meEFlxaCljgrS7x0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vZUxS/btsMT6EUTMo/RTJP8meEFlxaCljgrS7x0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vZUxS/btsMT6EUTMo/RTJP8meEFlxaCljgrS7x0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvZUxS%2FbtsMT6EUTMo%2FRTJP8meEFlxaCljgrS7x0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;69&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;69&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;2) EC2에 접속한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;key.pem이 있는 폴더에서 다음과 같이 입력한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;ssh -i key.pem ubuntu@EC2_IP&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게&amp;nbsp;Welcome to Ubuntu~라는 글자가 나오면 정상적으로 접속된 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCfh2G/btsMT9VUIFL/qMLZU6m7O7CKqMyMWzrWM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCfh2G/btsMT9VUIFL/qMLZU6m7O7CKqMyMWzrWM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCfh2G/btsMT9VUIFL/qMLZU6m7O7CKqMyMWzrWM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCfh2G%2FbtsMT9VUIFL%2FqMLZU6m7O7CKqMyMWzrWM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;635&quot; height=&quot;648&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;3) EC2에서 Spring boot 서버를 백그라운드로 실행한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;우선&amp;nbsp;ls&amp;nbsp;명령어를 통해 실제로 EC2에&amp;nbsp;.jar&amp;nbsp;파일이 있는지 확인한다. 파일이 제대로 있다면 다음 명령어를 입력한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;nohup java -jar expert-0.0.1-SNAPSHOT.jar &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;nohup&amp;nbsp;: no hang up의 줄임말로, 터미널 연결이 끊겨도 프로세스가 죽지 않고 계속 실행된다. 로그는 기본적으로&amp;nbsp;nohup.out&amp;nbsp;파일에 저장된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;java -jar expert-0.0.1-SNAPSHOT.jar&amp;nbsp;: java 명령어로&amp;nbsp;.jar&amp;nbsp;파일을 실행한다. Tomcat이 내장되어있어&amp;nbsp;8080&amp;nbsp;포트에서 바로 실행된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&amp;amp;&amp;nbsp;: 해당 명령어를 백그라운드로 실행한다. 터미널에서 바로 다음 명령어를 입력할 수 있고, 실행된 프로세스는 PID를 부여받아 별도로 동작한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1099&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x91tS/btsMTZ0iVd0/ySiRfW4bkYkP6WtYVtt81K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x91tS/btsMTZ0iVd0/ySiRfW4bkYkP6WtYVtt81K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x91tS/btsMTZ0iVd0/ySiRfW4bkYkP6WtYVtt81K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx91tS%2FbtsMTZ0iVd0%2FySiRfW4bkYkP6WtYVtt81K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1099&quot; height=&quot;140&quot; data-origin-width=&quot;1099&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;[1]&amp;nbsp;: 백그라운드 작업 번호&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;16824&amp;nbsp;: 실행된 프로세스의 PID (Process ID)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;nohup: ignoring input and appending output to 'nohup.out'&amp;nbsp;: 터미널 입력을 무시하고 모든 출력(로그)을 nohup.out 파일에 저장하겠다는 의미&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;⚠️ 배포 과정에서 발생한 오류&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;MySQL 연결 실패 오류 발생&lt;/span&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bV6yyq/btsMTMmty2V/YKQZ5zUTk0NzfD9kpQjk1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bV6yyq/btsMTMmty2V/YKQZ5zUTk0NzfD9kpQjk1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bV6yyq/btsMTMmty2V/YKQZ5zUTk0NzfD9kpQjk1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbV6yyq%2FbtsMTMmty2V%2FYKQZ5zUTk0NzfD9kpQjk1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;330&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DB URL에서 localhost로 되어 있으면 EC2가 자기 자신을 찾다가 오류가 발생한다고 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이때는 RDS를 구축하지 않은 상태여서 일단 DB 연결 없이 서버만 실행하는 방향으로 시도했다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 아래와 같이&amp;nbsp;application.properties에서 DB 설정을 주석 처리한후 다시 빌드했다.&lt;/span&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLuioL/btsMUnsXQ5d/6ZiI7R0KBLzKFUwc1J5Cw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLuioL/btsMUnsXQ5d/6ZiI7R0KBLzKFUwc1J5Cw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLuioL/btsMUnsXQ5d/6ZiI7R0KBLzKFUwc1J5Cw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLuioL%2FbtsMUnsXQ5d%2F6ZiI7R0KBLzKFUwc1J5Cw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;148&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;8080 포트를 이미 다른 프로세스가 잡고 있는 문제 발생&lt;/span&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAzKRI/btsMSDKV7Go/pvKpkJG236Ko3Chv7Ha3V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAzKRI/btsMSDKV7Go/pvKpkJG236Ko3Chv7Ha3V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAzKRI/btsMSDKV7Go/pvKpkJG236Ko3Chv7Ha3V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAzKRI%2FbtsMSDKV7Go%2FpvKpkJG236Ko3Chv7Ha3V1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;270&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;새로 빌드하고 배포 후 다시 실행하려고 하자 위와 같은메세지가 나왔다. 기존에 실행한 java -jar 프로세스가 아직 살아있거나 두 번 실행해 포트 충돌이 난 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;ps -ef | grep java&amp;nbsp;명령어를 이용해 PID를 확인 후,&amp;nbsp;kill -9 [PID]&amp;nbsp;명령어로 기존 프로세스를 종료한 후, 다시 서버를 실행한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;접속이 안된다면 보안 규칙에서&amp;nbsp;8080&amp;nbsp;포트가 열려 있는지 확인한다. 만약 열려있지 않다면 다음과 같이 Inbound rules에&amp;nbsp;8080&amp;nbsp;포트를 추가한다.&lt;/span&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uA9Ui/btsMSTtkLLO/WFeTeK5HcWpY6XqdoscvRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uA9Ui/btsMSTtkLLO/WFeTeK5HcWpY6XqdoscvRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uA9Ui/btsMSTtkLLO/WFeTeK5HcWpY6XqdoscvRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuA9Ui%2FbtsMSTtkLLO%2FWFeTeK5HcWpY6XqdoscvRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;591&quot; height=&quot;666&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;nohup java -jar expert-0.0.1-SNAPSHOT.jar &amp;amp;&amp;nbsp;명령어 오류 :&amp;nbsp;nohup: failed to run command 'java': No such file or directory&amp;nbsp;라는 오류 메세지가 나타났다. EC2 인스턴스에 Java가 설치되어 있지 않아서 발생한 오류이다. 다음과 같은 명령어를 입력해 Java를 설치 후 다시 시도한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install openjdk-17-jdk -y&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;탄력적-ipelastic-ip-설정하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;탄력적 IP(Elastic IP) 설정하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;EC2 대시보드에서&amp;nbsp;Network &amp;amp; Security - Elastic IPs를 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;369&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tyu7f/btsMSFaPE1q/I9GfLzDihRDKk7MzBeYqb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tyu7f/btsMSFaPE1q/I9GfLzDihRDKk7MzBeYqb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tyu7f/btsMSFaPE1q/I9GfLzDihRDKk7MzBeYqb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftyu7f%2FbtsMSFaPE1q%2FI9GfLzDihRDKk7MzBeYqb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;277&quot; height=&quot;229&quot; data-origin-width=&quot;369&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Allocate Elastic IP address&amp;nbsp;클릭하고 Amazon에서 제공하는 IP를 선택 후 Allocate 한다.&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;1280&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k9TZ1/btsMSRoKhIN/3cSDrujtuo8JFcQhSrlZa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k9TZ1/btsMSRoKhIN/3cSDrujtuo8JFcQhSrlZa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k9TZ1/btsMSRoKhIN/3cSDrujtuo8JFcQhSrlZa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk9TZ1%2FbtsMSRoKhIN%2F3cSDrujtuo8JFcQhSrlZa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;604&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;생성된 Elastic IP를 선택하고&amp;nbsp;Actions - Associate Elastic IP address를 누른다.&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;1280&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/epbYOA/btsMSnIoLZu/VPXbWeRCa2jrahKp4Jl6y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/epbYOA/btsMSnIoLZu/VPXbWeRCa2jrahKp4Jl6y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/epbYOA/btsMSnIoLZu/VPXbWeRCa2jrahKp4Jl6y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FepbYOA%2FbtsMSnIoLZu%2FVPXbWeRCa2jrahKp4Jl6y1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;179&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;연결할 인스턴스를 선택 후 Associate 한다.&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;1280&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXtwL8/btsMS9oWNQY/Oze2jQEUrkUJOLcmKLKYOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXtwL8/btsMS9oWNQY/Oze2jQEUrkUJOLcmKLKYOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXtwL8/btsMS9oWNQY/Oze2jQEUrkUJOLcmKLKYOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXtwL8%2FbtsMS9oWNQY%2FOze2jQEUrkUJOLcmKLKYOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;501&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;501&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이제 Public IP 대신 Elastic IP를 사용하면 된다. Elastic IP를 사용하면 인스턴스를 재시작해도 IP가 변하지 않고, Route53이나 외부 DNS에서 연결을 쉽게 할 수 있다. 그런데 연결이 안 된 EIP는 비용이 발생한다고 하니 꼭 인스턴스 연결 상태를 확인하자!&lt;/span&gt;&lt;/p&gt;
&lt;h1 id=&quot;rds&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;RDS&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@3uomlkh/AWS-RDS-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0&quot;&gt;RDS 인스턴스를 생성&lt;/a&gt;&amp;nbsp;했다면, EC2와 RDS를 연결해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;보안-그룹-설정하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;보안 그룹 설정하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기존에 EC2가 속한 보안그룹을 RDS에 추가한다. 나는 launch-wizard-2 라는 이름의 보안 그룹을 EC2에 적용했으므로 RDS에도 같은 것을 적용했다.&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;1280&quot; data-origin-height=&quot;359&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lIr5D/btsMUqws62f/ZA4b8kHJ7wp4XsPdsrUMB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lIr5D/btsMUqws62f/ZA4b8kHJ7wp4XsPdsrUMB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lIr5D/btsMUqws62f/ZA4b8kHJ7wp4XsPdsrUMB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlIr5D%2FbtsMUqws62f%2FZA4b8kHJ7wp4XsPdsrUMB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;359&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;359&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그리고 RDS 보안 그룹 Inbound rules에 MySQL/Aurora 포트(TCP 3306)를 추가했다. RDS 인스턴스는 AWS 내에서 격리되어 있기 때문에 외부에서 바로 접근할 수 없다. 따라서 RDS의 보안 그룹에서&amp;nbsp;&lt;b&gt;MySQL/Aurora 포트(3306)를 허용&lt;/b&gt;해야, EC2 인스턴스의 보안 그룹 또는 특정 IP가 RDS와 연결될 수 있다.&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;1280&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFgxQW/btsMSE345bi/rfyIiTrnWP6yC2UCJMLzi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFgxQW/btsMSE345bi/rfyIiTrnWP6yC2UCJMLzi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFgxQW/btsMSE345bi/rfyIiTrnWP6yC2UCJMLzi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFgxQW%2FbtsMSE345bi%2FrfyIiTrnWP6yC2UCJMLzi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;808&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;473&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWzxJd/btsMSQXHfN7/Nkt5hUO1YCWMXaijKh1Pak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWzxJd/btsMSQXHfN7/Nkt5hUO1YCWMXaijKh1Pak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWzxJd/btsMSQXHfN7/Nkt5hUO1YCWMXaijKh1Pak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWzxJd%2FbtsMSQXHfN7%2FNkt5hUO1YCWMXaijKh1Pak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;473&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;473&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;rds-연결-정보-작성하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;RDS 연결 정보 작성하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;application.properties에 다음과 같이 RDS 연결 정보를 작성했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;spring.datasource.url=jdbc:mysql://&amp;lt;엔드포인트&amp;gt;:&amp;lt;포트&amp;gt;/&amp;lt;DB명&amp;gt;
spring.datasource.username=admin
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;spring.datasource.url&amp;nbsp;: RDS 엔드포인트 + 포트 + DB명&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;spring.datasource.username&amp;nbsp;: RDS 사용자명&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;spring.datasource.password&amp;nbsp;: RDS 비밀번호&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;driver-class-name&amp;nbsp;; MySQL 사용 시 추가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;참고로 RDS 엔드포인트는 RDS 내 데이터베이스에 들어가면 나와있다.&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;1280&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tVBnm/btsMSzhyUFh/ebg6XCkl2wbwPsg4kVioS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tVBnm/btsMSzhyUFh/ebg6XCkl2wbwPsg4kVioS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tVBnm/btsMSzhyUFh/ebg6XCkl2wbwPsg4kVioS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtVBnm%2FbtsMSzhyUFh%2Febg6XCkl2wbwPsg4kVioS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;458&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;다시-빌드-후-배포하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다시 빌드 후 배포하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이제 위에서 EC2로 배포할 때 했던 과정을 다시 해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;빌드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;EC2로 새로 빌드한&amp;nbsp;.jar&amp;nbsp;파일 전송&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;EC2 접속 후 새 버전 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;로그 확인 or 브라우저 테스트&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;health-check-api로-확인하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;health check API로 확인하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;health check를 위해 다음 의존성을 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-actuator'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음은&amp;nbsp;application.properties에 추가하는 설정으로, RDS 연결 확인을 위해 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;management.endpoint.health.show-details=always
management.health.db.enabled=true&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;management.endpoint.health.show-details=always&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기본값은&amp;nbsp;when-authorized로, 권한이 없으면&amp;nbsp;status: &quot;UP&quot;만 보여준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DB, Disk, Custom Health 등 상세 정보까지 모두 출력한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;management.health.db.enabled=true&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Boot Actuator에서 DataSource 연결(DB Health Check) 여부를 켜는 설정이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DB 연결 상태를 확인하고 싶을 때 true로 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;health check로 확인해보면 다음과 같이 tjqjdmlDB가 정상적으로 돌아가고 있는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d01gTr/btsMTcsrEeR/mQPARY2GT0yJ7dhvmFnp3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d01gTr/btsMTcsrEeR/mQPARY2GT0yJ7dhvmFnp3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d01gTr/btsMTcsrEeR/mQPARY2GT0yJ7dhvmFnp3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd01gTr%2FbtsMTcsrEeR%2FmQPARY2GT0yJ7dhvmFnp3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;610&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위 결과에서 기본 경로가&amp;nbsp;/actuator/health가 아니라 그냥&amp;nbsp;/health인데, 아래 설정을 추가하면&amp;nbsp;/actuator&amp;nbsp;없이 루트 경로로 health check가 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;management.endpoints.web.base-path=/&lt;/code&gt;&lt;/pre&gt;</description>
      <category>AWS</category>
      <category>EC2</category>
      <category>healthcheck</category>
      <category>RDS</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/382</guid>
      <comments>https://3uomlkh.tistory.com/382#entry382comment</comments>
      <pubDate>Sat, 22 Mar 2025 00:05:22 +0900</pubDate>
    </item>
    <item>
      <title>[AWS]  S3 시작하기 - bucket 만들기, bucket policy 설정, 정적 웹 사이트 호스팅, EC2에 S3 권한 주기</title>
      <link>https://3uomlkh.tistory.com/381</link>
      <description>&lt;h1 id=&quot;s3simple-storage-service란&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;S3(Simple Storage Service)란?&lt;/b&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Amazon S3는 정적 웹 사이트 호스팅, 온라인 백업, 데이터 아카이브, 기업 애플리케이션, Big Data 분석 등 다양한 용도로 사용된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;버킷(Buckets)&lt;/b&gt; : 데이터를 저장하는 가장 상위 레벨의 폴더 형태의 컨테이너. S3에 저장되는 파일들을 &amp;lsquo;객체&amp;rsquo;라고 부르고, 모든 객체는 &amp;lsquo;키&amp;rsquo;(디렉토리)로 식별된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;대문자와 언더스코어 사용 금지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;길이 제한: 3자 이상, 63자 이하로 작성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;소문자, 숫자, 점(.) 그리고 하이픈(-)만 사용 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;반드시 문자 또는 숫자로 시작하고 끝내기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;두 개의 점(.) 연속 사용 금지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;IP 주소 형식 사용 금지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;s3-시작하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;S3 시작하기&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;h2 id=&quot;버킷-만들기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;버킷 만들기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Create bucket을 눌러 버킷을 생성해보자! &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이름만 정하고 다른 옵션은 일단 그대로 두었다. 버킷의 이름은 global unique 하기 때문에 이미 있는 이름인 경우가 많다. 버킷 생성이 안된다면 이름 때문인지 확인해보자.&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;1280&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B0eAN/btsMSkC1CLL/Wuh84TksXk0pkJwaS2Sk10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B0eAN/btsMSkC1CLL/Wuh84TksXk0pkJwaS2Sk10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B0eAN/btsMSkC1CLL/Wuh84TksXk0pkJwaS2Sk10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB0eAN%2FbtsMSkC1CLL%2FWuh84TksXk0pkJwaS2Sk10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;302&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;버킷 생성이 완료되면 다음과 같이 리스트에 나타난다. 해당 버킷의 AWS Resion은 서울인 것을 알 수 있고, 다른 Regon인 버킷이 추가되더라도 이 리스트에서 모두 볼 수 있다.&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;1280&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sjdUO/btsMQnVMNYT/yTBhJ4V5if5FWJRFIQH6RK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sjdUO/btsMQnVMNYT/yTBhJ4V5if5FWJRFIQH6RK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sjdUO/btsMQnVMNYT/yTBhJ4V5if5FWJRFIQH6RK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsjdUO%2FbtsMQnVMNYT%2FyTBhJ4V5if5FWJRFIQH6RK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;284&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;버킷에-파일-직접-업로드-하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;버킷에 파일 직접 업로드 하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;버킷 안에 들어가면 다음과 같이 되어있는데, 맨 오른쪽&amp;nbsp;Upload를 누른다.&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;1280&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J0xSt/btsMRfv3NxF/kglrKNQJnmAlcAjpbDFZN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J0xSt/btsMRfv3NxF/kglrKNQJnmAlcAjpbDFZN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J0xSt/btsMRfv3NxF/kglrKNQJnmAlcAjpbDFZN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ0xSt%2FbtsMRfv3NxF%2FkglrKNQJnmAlcAjpbDFZN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;300&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;들어가서&amp;nbsp;Add File을 통해 아무 파일이나 올린 후&amp;nbsp;Upload를 누른다.&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;1280&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6Rs3T/btsMSnmebx3/pIyIjvGLRNmdbg6CCbxOD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6Rs3T/btsMSnmebx3/pIyIjvGLRNmdbg6CCbxOD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6Rs3T/btsMSnmebx3/pIyIjvGLRNmdbg6CCbxOD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6Rs3T%2FbtsMSnmebx3%2FpIyIjvGLRNmdbg6CCbxOD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;528&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1280&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8acju/btsMRn1HP4o/9PM6A9FlYlUNtBtxuButuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8acju/btsMRn1HP4o/9PM6A9FlYlUNtBtxuButuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8acju/btsMRn1HP4o/9PM6A9FlYlUNtBtxuButuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8acju%2FbtsMRn1HP4o%2F9PM6A9FlYlUNtBtxuButuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;440&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;파일명을 눌러보면 다음과 같이 뜨는데, Object URL과 Open을 각각 눌러서 비교해보자.&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;1280&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbmAkR/btsMQpswgB1/1frvMplzYhkiJ7qHsVQgyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbmAkR/btsMQpswgB1/1frvMplzYhkiJ7qHsVQgyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbmAkR/btsMQpswgB1/1frvMplzYhkiJ7qHsVQgyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbmAkR%2FbtsMQpswgB1%2F1frvMplzYhkiJ7qHsVQgyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;540&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;각각 눌러서 보니 다음과 같은 차이점이 있다. 현재 접근 권한이 없어 아래과 같이 나오는데, 왜 Open을 누르면 제대로 보일까? 이 있는데. Open을 누르면 S3에서 만들어진 &lt;b&gt;pre-signed URL&lt;/b&gt;이라는 것을 자동으로 만들어준다. pre-signed URL은 권한이 없어도 권한을 만들어주는 URL이라서 두번째처럼 접근이 가능하다.&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;1280&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/da53Dj/btsMQDEdGFR/AdrtNsucH6om17Gz71SXW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/da53Dj/btsMQDEdGFR/AdrtNsucH6om17Gz71SXW0/img.png&quot; data-alt=&quot;Object URL로 들어갔을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/da53Dj/btsMQDEdGFR/AdrtNsucH6om17Gz71SXW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fda53Dj%2FbtsMQDEdGFR%2FAdrtNsucH6om17Gz71SXW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;265&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Object URL로 들어갔을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Et40o/btsMRWbslwP/cCj1FG1ocpqqfkwxL3QJr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Et40o/btsMRWbslwP/cCj1FG1ocpqqfkwxL3QJr0/img.png&quot; data-alt=&quot;Open으로 들어갔을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Et40o/btsMRWbslwP/cCj1FG1ocpqqfkwxL3QJr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEt40o%2FbtsMRWbslwP%2FcCj1FG1ocpqqfkwxL3QJr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;659&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;659&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Open으로 들어갔을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;폴더-만들기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;폴더 만들기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이제 폴더도 만들어보자. 다시 버킷에 들어가서 Create folder를 누른다.&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;1280&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNvx0A/btsMSaN4LBO/HuSGUc1cgjEu4lXpWRwzn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNvx0A/btsMSaN4LBO/HuSGUc1cgjEu4lXpWRwzn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNvx0A/btsMSaN4LBO/HuSGUc1cgjEu4lXpWRwzn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNvx0A%2FbtsMSaN4LBO%2FHuSGUc1cgjEu4lXpWRwzn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;320&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;폴더 이름을 적을 때는&amp;nbsp;/가 뒤에 자동으로 들어가기 때문에&amp;nbsp;/를 붙이지 않아도 된다. 폴더 이름을 적고&amp;nbsp;Create folder를 누른다.&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;1280&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oX1iC/btsMQpMSI3i/8ffNCNdUHyeuuTHygGKIO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oX1iC/btsMQpMSI3i/8ffNCNdUHyeuuTHygGKIO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oX1iC/btsMQpMSI3i/8ffNCNdUHyeuuTHygGKIO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoX1iC%2FbtsMQpMSI3i%2F8ffNCNdUHyeuuTHygGKIO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;583&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;583&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1280&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctjdxi/btsMQdy5vnE/dPaQSHOKcCKudxZPLfbykk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctjdxi/btsMQdy5vnE/dPaQSHOKcCKudxZPLfbykk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctjdxi/btsMQdy5vnE/dPaQSHOKcCKudxZPLfbykk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fctjdxi%2FbtsMQdy5vnE%2FdPaQSHOKcCKudxZPLfbykk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;308&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;버킷-폴리시bucket-policy&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;버킷 폴리시(Bucket Policy)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id=&quot;버킷-폴리시란&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;버킷 폴리시란?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;버킷 폴리시(Bucket Policy)&lt;/b&gt; : IAM 정책과 유사한 JSON 형식의 문서. &lt;b&gt;S3 버킷과 그 안의 객체에 대한 접근 권한을 제어&lt;/b&gt;하는 역할을 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;버킷 내 모든 객체에 대한 접근 제어&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;특정 객체 또는 특정 객체 그룹에 대한 접근 제어&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;허용할 IP 주소 또는 IP 대역을 지정해 특정 위치에서만 접근이 가능하도록 설정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;접근할 수 있는 리소스의 범위를 지정해 필요한 리소스만 접근하도록 제한&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;암호화된 연결(SSL/TLS)을 사용하도록 강제 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;버킷-폴리시-적용하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;버킷 폴리시 적용하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;버킷에서 Permissions에 들어간다.&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;1280&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NNaNP/btsMQavBsVn/4VDfSqTMXjHyJNwaptw7u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NNaNP/btsMQavBsVn/4VDfSqTMXjHyJNwaptw7u1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NNaNP/btsMQavBsVn/4VDfSqTMXjHyJNwaptw7u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNNaNP%2FbtsMQavBsVn%2F4VDfSqTMXjHyJNwaptw7u1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;260&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Block public access도 모두 on이고, Bucket policy도 아예 작동하지 않는 것을 볼 수 있다.&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;1280&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbDgWR/btsMQHGpbhz/9yaCEK66gnCbBDPAA2Kn2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbDgWR/btsMQHGpbhz/9yaCEK66gnCbBDPAA2Kn2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbDgWR/btsMQHGpbhz/9yaCEK66gnCbBDPAA2Kn2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbDgWR%2FbtsMQHGpbhz%2F9yaCEK66gnCbBDPAA2Kn2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;540&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;먼저 &lt;b&gt;Block public access&lt;/b&gt;에 들어가서 &lt;b&gt;Block all public access에 되어있던 체크박스를 해제&lt;/b&gt;한다. 버킷을 만들 때 있었던 부분인데 이렇게 따로 설정을 바꿀 수 있다. 설정을 바꾼 후&amp;nbsp;&lt;b&gt;Save changes&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;1280&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwDA0Q/btsMSagdGoo/0LcLI5WkwpQDAWkf0xAPFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwDA0Q/btsMSagdGoo/0LcLI5WkwpQDAWkf0xAPFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwDA0Q/btsMSagdGoo/0LcLI5WkwpQDAWkf0xAPFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwDA0Q%2FbtsMSagdGoo%2F0LcLI5WkwpQDAWkf0xAPFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;361&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그럼 이렇게 off 된 모습을 볼 수 있다.&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;1280&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/db7IIm/btsMQcmDi7H/jZa9M4XQ4CzggMLF7TUfQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/db7IIm/btsMQcmDi7H/jZa9M4XQ4CzggMLF7TUfQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/db7IIm/btsMQcmDi7H/jZa9M4XQ4CzggMLF7TUfQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdb7IIm%2FbtsMQcmDi7H%2FjZa9M4XQ4CzggMLF7TUfQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;170&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이제 Bucket policy에 들어가서 &lt;b&gt;Bucket ARN&lt;/b&gt;을 복사한 후,&amp;nbsp;&lt;b&gt;Policy Generator&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;1280&quot; data-origin-height=&quot;553&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JnHms/btsMQdsmPiP/koR3mgefkIxv9xjZ5KFz4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JnHms/btsMQdsmPiP/koR3mgefkIxv9xjZ5KFz4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JnHms/btsMQdsmPiP/koR3mgefkIxv9xjZ5KFz4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJnHms%2FbtsMQdsmPiP%2FkoR3mgefkIxv9xjZ5KFz4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;553&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;553&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Policy Generator&lt;/b&gt;에서 다음과 같이 설정해준다. Principal은 전체를 대상으로 하기 위해&lt;b&gt;&amp;nbsp;*&lt;/b&gt;&amp;nbsp;를 적었다. ARN에는 들어오기 전 복사한 ARN을 붙여넣기 한다.&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;1230&quot; data-origin-height=&quot;889&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dx9zSI/btsMRLutidj/7ebp4NeFzrbbT34fiqnluk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dx9zSI/btsMRLutidj/7ebp4NeFzrbbT34fiqnluk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dx9zSI/btsMRLutidj/7ebp4NeFzrbbT34fiqnluk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdx9zSI%2FbtsMRLutidj%2F7ebp4NeFzrbbT34fiqnluk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1230&quot; height=&quot;889&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;889&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Actions는 줄 권한을 설정하면 되는데, 나는 읽기 권한을 위해 &lt;b&gt;GetObject&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;1280&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDAb0Y/btsMSkXk41H/JiEZTOryAbkvUf7WYkUWw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDAb0Y/btsMSkXk41H/JiEZTOryAbkvUf7WYkUWw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDAb0Y/btsMSkXk41H/JiEZTOryAbkvUf7WYkUWw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDAb0Y%2FbtsMSkXk41H%2FJiEZTOryAbkvUf7WYkUWw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;631&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다 작성했다면&amp;nbsp;&lt;b&gt;Add Statement&lt;/b&gt;를 누르고,&amp;nbsp;&lt;b&gt;Generate Policy&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;1246&quot; data-origin-height=&quot;725&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/367D9/btsMQEJJW8D/pkeMR7sXo2jgukvLD6t9BK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/367D9/btsMQEJJW8D/pkeMR7sXo2jgukvLD6t9BK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/367D9/btsMQEJJW8D/pkeMR7sXo2jgukvLD6t9BK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F367D9%2FbtsMQEJJW8D%2FpkeMR7sXo2jgukvLD6t9BK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1246&quot; height=&quot;725&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;725&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그럼 다음과 같은 JSON 형식의 Policy 문서가 뜨는데, 복사해서 &lt;b&gt;Bucket policy&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;799&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZNDQ9/btsMQedz6Ek/zwT1CU01FjKblTeUzUW3I1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZNDQ9/btsMQedz6Ek/zwT1CU01FjKblTeUzUW3I1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZNDQ9/btsMQedz6Ek/zwT1CU01FjKblTeUzUW3I1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZNDQ9%2FbtsMQedz6Ek%2FzwT1CU01FjKblTeUzUW3I1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;799&quot; height=&quot;496&quot; data-origin-width=&quot;799&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj5wkO/btsMRwEdBpA/EkyygZwqI4KEL7WdZ3YwW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj5wkO/btsMRwEdBpA/EkyygZwqI4KEL7WdZ3YwW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj5wkO/btsMRwEdBpA/EkyygZwqI4KEL7WdZ3YwW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj5wkO%2FbtsMRwEdBpA%2FEkyygZwqI4KEL7WdZ3YwW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;470&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여기서 수정해야 할 점은 버킷 안에 있는 모든 것에 대한 권한이기 때문에&amp;nbsp;&quot;Resource&quot;: &quot;arn:aws:s3:::my-gom-bucket&quot;가 아니라&amp;nbsp;&quot;Resource&quot;: &quot;arn:aws:s3:::my-gom-bucket/*&quot;로 바꿔야 한다.&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;693&quot; data-origin-height=&quot;509&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCbXl4/btsMQabgH9C/iJsFNCJxlkqAHG9eX0Wxnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCbXl4/btsMQabgH9C/iJsFNCJxlkqAHG9eX0Wxnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCbXl4/btsMQabgH9C/iJsFNCJxlkqAHG9eX0Wxnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCbXl4%2FbtsMQabgH9C%2FiJsFNCJxlkqAHG9eX0Wxnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;509&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;509&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 모두 설정을 마치고 Object URL로 들어가니 이전과 달리 이미지가 잘 나오는 모습을 볼 수 있다.&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;1280&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czY604/btsMSoyFasn/kUN9LkznVLxrAkVJU5iK71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czY604/btsMSoyFasn/kUN9LkznVLxrAkVJU5iK71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czY604/btsMSoyFasn/kUN9LkznVLxrAkVJU5iK71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczY604%2FbtsMSoyFasn%2FkUN9LkznVLxrAkVJU5iK71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;648&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;정적-웹-사이트-호스팅&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;정적 웹 사이트 호스팅&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;정적 웹 사이트 호스팅을 해보기 위해 html 파일을 버킷에 업로드했다.&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;1280&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FTZZn/btsMQHsWRbH/J9AdAKxNbFKfscXFM9xe80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FTZZn/btsMQHsWRbH/J9AdAKxNbFKfscXFM9xe80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FTZZn/btsMQHsWRbH/J9AdAKxNbFKfscXFM9xe80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFTZZn%2FbtsMQHsWRbH%2FJ9AdAKxNbFKfscXFM9xe80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;274&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;업로드 후 버킷에서 Properties에 들어간다.&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;1280&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7jPYX/btsMQnamOia/IGvMgQEtWFXfH6HGsNS3TK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7jPYX/btsMQnamOia/IGvMgQEtWFXfH6HGsNS3TK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7jPYX/btsMQnamOia/IGvMgQEtWFXfH6HGsNS3TK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7jPYX%2FbtsMQnamOia%2FIGvMgQEtWFXfH6HGsNS3TK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;234&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;맨 아래 Static website hosting에서 Edit을 누른다.&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;1280&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCQc3p/btsMQ4g590f/2WHuQGxzRqMEHIt0YR4cA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCQc3p/btsMQ4g590f/2WHuQGxzRqMEHIt0YR4cA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCQc3p/btsMQ4g590f/2WHuQGxzRqMEHIt0YR4cA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCQc3p%2FbtsMQ4g590f%2F2WHuQGxzRqMEHIt0YR4cA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;191&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이 세팅을 해준다. index document에서는 내가 올린 html 파일명을 적어준다.&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;1280&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RBxGc/btsMQGU6S0Y/D6G1ptPUsKnwDbCxFIRyjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RBxGc/btsMQGU6S0Y/D6G1ptPUsKnwDbCxFIRyjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RBxGc/btsMQGU6S0Y/D6G1ptPUsKnwDbCxFIRyjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRBxGc%2FbtsMQGU6S0Y%2FD6G1ptPUsKnwDbCxFIRyjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;400&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그럼 이렇게 Bucket website endpoint가 뜨게 되는데, 들어가보면 내가 올린 html 파일의 내용과 같다는 것을 알 수 있다. 만약 제대로 뜨지 않는다면 Bucket policy를 잘 설정했는지 확인하자.&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;1280&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F0yHT/btsMP8dDZoB/MslumfDysmAvEcFKdZiqhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F0yHT/btsMP8dDZoB/MslumfDysmAvEcFKdZiqhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F0yHT/btsMP8dDZoB/MslumfDysmAvEcFKdZiqhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF0yHT%2FbtsMP8dDZoB%2FMslumfDysmAvEcFKdZiqhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;300&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1280&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYqahi/btsMQHl7roa/oQi4GJQknxr6DkGfyJoZs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYqahi/btsMQHl7roa/oQi4GJQknxr6DkGfyJoZs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYqahi/btsMQHl7roa/oQi4GJQknxr6DkGfyJoZs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYqahi%2FbtsMQHl7roa%2FoQi4GJQknxr6DkGfyJoZs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;137&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;137&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;ec2에-s3-권한-주기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;EC2에 S3 권한 주기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;EC2에 S3 권한을 주기 위해서는 IAM Role을 적용해줘야 한다.&amp;nbsp;IAM - Role에 들어가서&amp;nbsp;Create role을 클릭한다.&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;1280&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/colv9n/btsMQ7Zec1d/PMwagUFSp15dUhI1jQeVHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/colv9n/btsMQ7Zec1d/PMwagUFSp15dUhI1jQeVHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/colv9n/btsMQ7Zec1d/PMwagUFSp15dUhI1jQeVHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcolv9n%2FbtsMQ7Zec1d%2FPMwagUFSp15dUhI1jQeVHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;309&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;309&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Use case에 EC2 선택 후 다음으로 넘어간다.&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;1213&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJZhK/btsMRul5Bd0/rn7wHJG1fqgTiPc2kQKvA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJZhK/btsMRul5Bd0/rn7wHJG1fqgTiPc2kQKvA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJZhK/btsMRul5Bd0/rn7wHJG1fqgTiPc2kQKvA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJZhK%2FbtsMRul5Bd0%2Frn7wHJG1fqgTiPc2kQKvA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1213&quot; height=&quot;481&quot; data-origin-width=&quot;1213&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;검색창에 S3를 입력 후&amp;nbsp;AmazonS3FullAccess를 체크한다. 혹은 특정 버킷에만 권한을 주는 커스텀 정책을 만들 수도 있다. 그 다음 Role의 이름을 정하고&amp;nbsp;Create role을 눌러 생성한다.&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;1280&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfow48/btsMRSG0y5e/bxztFuuRnVg7wg1vDfCFn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfow48/btsMRSG0y5e/bxztFuuRnVg7wg1vDfCFn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfow48/btsMRSG0y5e/bxztFuuRnVg7wg1vDfCFn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfow48%2FbtsMRSG0y5e%2FbxztFuuRnVg7wg1vDfCFn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;522&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이 새로운 role이 생성된 것을 볼 수 있다.&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;1280&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by80vo/btsMRu0GrHc/SUHMhCYq7VN3k82jPoxlD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by80vo/btsMRu0GrHc/SUHMhCYq7VN3k82jPoxlD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by80vo/btsMRu0GrHc/SUHMhCYq7VN3k82jPoxlD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby80vo%2FbtsMRu0GrHc%2FSUHMhCYq7VN3k82jPoxlD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;310&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이제 내 EC2 인스턴스로 와서&amp;nbsp;&lt;b&gt;Actions - Security - Modify IAM Role&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;1280&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc0H6U/btsMQCk2nT2/7retAnildZgR31usIWgmj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc0H6U/btsMQCk2nT2/7retAnildZgR31usIWgmj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc0H6U/btsMQCk2nT2/7retAnildZgR31usIWgmj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc0H6U%2FbtsMQCk2nT2%2F7retAnildZgR31usIWgmj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;255&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;방금 만든&amp;nbsp;ec2-s3-access-role을 선택하고,&amp;nbsp;&lt;b&gt;Update IAM Role&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;1280&quot; data-origin-height=&quot;227&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o7YtJ/btsMRvywyus/rRkQqNQIVO2t740bI7nsNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o7YtJ/btsMRvywyus/rRkQqNQIVO2t740bI7nsNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o7YtJ/btsMRvywyus/rRkQqNQIVO2t740bI7nsNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo7YtJ%2FbtsMRvywyus%2FrRkQqNQIVO2t740bI7nsNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;227&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;227&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그럼 이제 EC2가 S3 접근 권한을 갖게 되었다. 따라서&amp;nbsp;application.properties(혹은&amp;nbsp;application.yml)에&amp;nbsp;access-key와&amp;nbsp;secret-key를 작성할 필요가 없다. Role을 달면 EC2 인스턴스 메타데이터에서 자동으로 임시 키를 발급받아서 AWS 접근하게 된다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>AWS</category>
      <category>S3</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/381</guid>
      <comments>https://3uomlkh.tistory.com/381#entry381comment</comments>
      <pubDate>Fri, 21 Mar 2025 00:50:03 +0900</pubDate>
    </item>
    <item>
      <title>[Java/Spring] 인터페이스 vs enum 클래스 vs final 클래스 - 상수 관리하는 법</title>
      <link>https://3uomlkh.tistory.com/380</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;지난 프로젝트에서 다른 팀원 분이 상수를 인터페이스로 관리하는 것을 봤다. final 클래스나 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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;예시 코드는 내가 작성한 에러 메세지 enum 클래스인데, GPT한테 각각의 경우에 맞게 수정을 해달라고 해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;final 클래스로 상수 관리하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;상수를 관리하려고 할 때 사용되는 가장 고전적인 방법이 아닐까 싶다. final 클래스를 만들고 , 상수들을 static final로 선언한다. 이렇게 하면 &lt;span style=&quot;text-align: justify;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;정적 import를 통해 클래스 이름을 생략해서 사용할 수 있다는 장점이 있다. &lt;/span&gt;하지만 확장성과 유지보수성 측면에서 enum보다 떨어진다. 만약 final 클래스로 상수를 관리한다면 기본 생성자를 private으로 설정해 인스턴스화를 방지하는 것을 잊지 말자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741950608534&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class ErrorOrderMessages {
    public static final String CANNOT_CANCEL_ORDER = &quot;취소할 수 없는 주문입니다.&quot;;
    public static final String ONLY_USER_CAN_CANCEL = &quot;본인의 주문만 취소할 수 있습니다.&quot;;
    public static final String ORDER_NOT_FOUND = &quot;주문 내역이 존재하지 않습니다.&quot;;
    public static final String MINIMUM_ORDER_NOT_MET = &quot;최소 주문 금액 이상 주문해야 합니다.&quot;;
    public static final String NOT_BUSINESS_HOURS = &quot;현재 영업 시간이 아닙니다.&quot;;

    public static final String NOT_OWNER_ORDER = &quot;본인 가게 주문만 처리할 수 있습니다.&quot;;
    public static final String ALREADY_DELIVERING_OR_COMPLETED = &quot;이미 배달이 시작되었거나 완료된 주문입니다.&quot;;
    public static final String MUST_BE_ACCEPTED_BEFORE_COOKING = &quot;주문 접수 후에 조리를 시작할 수 있습니다.&quot;;
    public static final String MUST_BE_COOKING_BEFORE_DELIVERY = &quot;조리를 시작한 주문만 배달할 수 있습니다.&quot;;
    public static final String MUST_BE_DELIVERING_BEFORE_COMPLETE = &quot;배달중인 주문만 배달 완료 처리를 할 수 있습니다.&quot;;
    public static final String ALREADY_COMPLETED = &quot;이미 배달이 완료된 주문입니다.&quot;;

    private ErrorOrderMessages() {} // 인스턴스화 방지
}&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;튜터님은 상속 방지가 더 중요하다거나 캡슐화 자체가 중요하다면 final 클래스를 추천할 수도 있다고 하셨다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;interface로 상수 관리하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인터페이스의 역할이 뭘까? 인터페이스는 어떤 클래스를 구현할 때 틀을 제공해주는 역할을 한다. 하지만 아래와 같은 경우는 틀을 제공하는 것이 아니라 이미 정해놓은 상수를 모아두는 역할을 할 뿐이다. 이렇게 인터페이스의 역할을 벗어나면 이를 구현하는 클래스들 사이의 일관성이 떨어지게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741950578557&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface ErrorOrderMessage {
    String CANNOT_CANCEL_ORDER = &quot;취소할 수 없는 주문입니다.&quot;;
    String ONLY_USER_CAN_CANCEL = &quot;본인의 주문만 취소할 수 있습니다.&quot;;
    String ORDER_NOT_FOUND = &quot;주문 내역이 존재하지 않습니다.&quot;;
    String MINIMUM_ORDER_NOT_MET = &quot;최소 주문 금액 이상 주문해야 합니다.&quot;;
    String NOT_BUSINESS_HOURS = &quot;현재 영업 시간이 아닙니다.&quot;;

    String NOT_OWNER_ORDER = &quot;본인 가게 주문만 처리할 수 있습니다.&quot;;
    String ALREADY_DELIVERING_OR_COMPLETED = &quot;이미 배달이 시작되었거나 완료된 주문입니다.&quot;;
    String MUST_BE_ACCEPTED_BEFORE_COOKING = &quot;주문 접수 후에 조리를 시작할 수 있습니다.&quot;;
    String MUST_BE_COOKING_BEFORE_DELIVERY = &quot;조리를 시작한 주문만 배달할 수 있습니다.&quot;;
    String MUST_BE_DELIVERING_BEFORE_COMPLETE = &quot;배달중인 주문만 배달 완료 처리를 할 수 있습니다.&quot;;
    String ALREADY_COMPLETED = &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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;하지만 튜터님 중 한 분은 인터페이스를 선호하진 않지만, 상수만을 저장하는 용도라고 한다면 인터페이스도 많이 사용할 것 같다고 말씀해주셨다. 혹은 상수 재사용이 제일 중요하다 하면 인터페이스를 주로 쓸 수 있을 것이라고 하셨다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;enum 클래스로 상수 관리하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;만약 에러 메세지를 여러 언어로 제공해야 하는 경우가 생긴다면 어떨까? enum 클래스를 사용한다면 그냥 필드 하나만 추가해주면 돼서 유지보수가 더 쉽다. enum은 특히 잘못된 값을 방지할 수 있어 타입 안정성을 보장한다는 장점이 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741950597443&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public enum ErrorOrderMessage {
    CANNOT_CANCEL_ORDER(&quot;취소할 수 없는 주문입니다.&quot;),
    ONLY_USER_CAN_CANCEL(&quot;본인의 주문만 취소할 수 있습니다.&quot;),
    ORDER_NOT_FOUND(&quot;주문 내역이 존재하지 않습니다.&quot;),
    MINIMUM_ORDER_NOT_MET(&quot;최소 주문 금액 이상 주문해야 합니다.&quot;),
    NOT_BUSINESS_HOURS(&quot;현재 영업 시간이 아닙니다.&quot;),

    NOT_OWNER_ORDER(&quot;본인 가게 주문만 처리할 수 있습니다.&quot;),
    ALREADY_DELIVERING_OR_COMPLETED(&quot;이미 배달이 시작되었거나 완료된 주문입니다.&quot;),
    MUST_BE_ACCEPTED_BEFORE_COOKING(&quot;주문 접수 후에 조리를 시작할 수 있습니다.&quot;),
    MUST_BE_COOKING_BEFORE_DELIVERY(&quot;조리를 시작한 주문만 배달할 수 있습니다.&quot;),
    MUST_BE_DELIVERING_BEFORE_COMPLETE(&quot;배달중인 주문만 배달 완료 처리를 할 수 있습니다.&quot;),
    ALREADY_COMPLETED(&quot;이미 배달이 완료된 주문입니다.&quot;);

    private final String message;

    ErrorOrderMessage(String message) {
        this.message = message;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;타입 안정성 향상 : 사전에 정의된 상수들로만 구성되므로 유효한 값만 입력된다. 유효하지 않은 값이 들어오면 컴파일 오류가 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;간결성 및 일관성 : 코드가 더 간결해지고 명확해지며, 데이터 일관성이 보장된다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;확장성 : enum에 새로운 상수를 추가하기만 하면 간편하게 확장이 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;검색해서도 혹은 튜터님께 질문을 해서도 조금씩 의견이 다른 부분이 있었지만, 대부분 타입 안정성과 확장성 측면에서 enum을 주로 선호하는 것 같았다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;결론 : 그냥 enum 쓰자.&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Enum</category>
      <category>상수</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/380</guid>
      <comments>https://3uomlkh.tistory.com/380#entry380comment</comments>
      <pubDate>Wed, 19 Mar 2025 01:33:54 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] RDS 가볍게 시작해보기!</title>
      <link>https://3uomlkh.tistory.com/379</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #666666; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;스파르타코딩클럽 AWS의 모든 것 (All about AWS) 강의를 듣고 정리한 내용입니다.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;div style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;h1 id=&quot;rds란&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RDS란?&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h1&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Relational Database Service(RDS)&lt;/b&gt;&amp;nbsp;: AWS에서 제공하는 관리형 관계형 데이터베이스 서비스. Postgres, MySQL, MariaDB, Oracle, Microsoft SQL Server 등 다양한 엔진을 선택할 수 있으며, EC2에 직접 DB를 설치&amp;middot;운영하는 방식보다 운영 부담이 적다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;rds-특징&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RDS 특징&lt;/span&gt;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DB 설치, 패치, 유지보수 작업을 AWS가 수행한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;지속적인 자동 백업 지원&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;지정한 기간 내 언제든 복구 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;성능 모니터링과 대시보드 제공&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;스토리지 자동 확장 지원&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;VPC, IAM, KMS로 데이터 보안 강화&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;SSH 접근 불가&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;EC2처럼 SSH로 접근하지 않고 DB 툴 또는 애플리케이션에서 연결한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;storage-auto-scaling&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Storage Auto Scaling&lt;/span&gt;&lt;/b&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DB 용량의 한계치까지 왔을 때 자동으로 용량을 늘려준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;사용을 위해 Maximum Storage Threshold를 지정해줘야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;예측 불가능한 트래픽이 있을때 유용하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;rds-read-replicas&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;RDS Read Replicas&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mxPjW/btsMNJEsoZy/5sI0H2vIkeHiBSIsf6rDdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mxPjW/btsMNJEsoZy/5sI0H2vIkeHiBSIsf6rDdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mxPjW/btsMNJEsoZy/5sI0H2vIkeHiBSIsf6rDdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmxPjW%2FbtsMNJEsoZy%2F5sI0H2vIkeHiBSIsf6rDdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;892&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;/figure&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;읽기 전용 복제본으로 읽기 부하를 분산시킬 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;비동기 복제로 운영된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;SELECT 쿼리만 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;INSERT, UPDATE, DELETE는 불가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;조회가 많은 서비스에서 성능 향상용으로 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;rds-multi-az&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;RDS Multi-AZ&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOGBGs/btsMOexpjAN/AWaYZK7fWfEUjtoYIdgorK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOGBGs/btsMOexpjAN/AWaYZK7fWfEUjtoYIdgorK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOGBGs/btsMOexpjAN/AWaYZK7fWfEUjtoYIdgorK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOGBGs%2FbtsMOexpjAN%2FAWaYZK7fWfEUjtoYIdgorK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;715&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;715&quot;/&gt;&lt;/span&gt;&lt;/figure&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;고가용성 확보를 위한 기능으로, 장애 발생 시 자동으로 대기 인스턴스로 전환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;수동 설정 없이 옵션 선택만으로 구성 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;가용성을 높이지만 확장성에는 영향을 주지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Read Replica 역시 Multi-AZ로 구성할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Failover 발생 시&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;DNS가 자동으로 대기 인스턴스로 연결 전환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;rds-시작하기&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;RDS 시작하기&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;AWS에서 RDS를 검색해 들어간다. 바로 Create database를 눌러 데이터베이스를 만들어보자.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LIttl/btsMOvS7Gg5/eMPIjRXYipaK4WiSto6kLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LIttl/btsMOvS7Gg5/eMPIjRXYipaK4WiSto6kLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LIttl/btsMOvS7Gg5/eMPIjRXYipaK4WiSto6kLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLIttl%2FbtsMOvS7Gg5%2FeMPIjRXYipaK4WiSto6kLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;584&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;제일 먼저 Standard create와 Easy create 중에서 고르면 되는데, 나는 강의를 따라 하느라 Standard로 골랐다. 그래서인지 옵션이 아주 많았는데 상황에 따라 맞게 고르면 될 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;123&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KsaEw/btsMOtOvJIC/NBvqV25rkj8eCYO5SvFWOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KsaEw/btsMOtOvJIC/NBvqV25rkj8eCYO5SvFWOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KsaEw/btsMOtOvJIC/NBvqV25rkj8eCYO5SvFWOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKsaEw%2FbtsMOtOvJIC%2FNBvqV25rkj8eCYO5SvFWOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;123&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;123&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그 다음은 어떤 DB를 쓸건지 고르는데, 나는 MySQL로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;641&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVKUNM/btsMNA1XNHm/BgdLELlLLbJQ7SqMSxi2j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVKUNM/btsMNA1XNHm/BgdLELlLLbJQ7SqMSxi2j1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVKUNM/btsMNA1XNHm/BgdLELlLLbJQ7SqMSxi2j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVKUNM%2FbtsMNA1XNHm%2FBgdLELlLLbJQ7SqMSxi2j1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;641&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;641&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Templates은 Free Tire로 했다. 개인 프로젝트라면 거의 Free Tire로 하면 될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wTTiq/btsMODwGyZA/vyunlu7v8q8INsdDogkVN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wTTiq/btsMODwGyZA/vyunlu7v8q8INsdDogkVN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wTTiq/btsMODwGyZA/vyunlu7v8q8INsdDogkVN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwTTiq%2FbtsMODwGyZA%2Fvyunlu7v8q8INsdDogkVN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;143&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Settings에서는 내 DB 인스턴스 이름과 master username, password를 정한다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;637&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PA1Kl/btsMObAIelH/n4Towym81QTgUJNWD8Gdik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PA1Kl/btsMObAIelH/n4Towym81QTgUJNWD8Gdik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PA1Kl/btsMObAIelH/n4Towym81QTgUJNWD8Gdik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPA1Kl%2FbtsMObAIelH%2Fn4Towym81QTgUJNWD8Gdik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;637&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;637&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Storage에서는 다음과 같이 설정했고, 만약 DB가 20GB에 가깝게 차면 autoscaling해서 1000GB까지 늘린다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvBsjR/btsMO6k0EQX/Cu0KeWfUwlHhpnwFJtrjd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvBsjR/btsMO6k0EQX/Cu0KeWfUwlHhpnwFJtrjd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvBsjR/btsMO6k0EQX/Cu0KeWfUwlHhpnwFJtrjd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvBsjR%2FbtsMO6k0EQX%2FCu0KeWfUwlHhpnwFJtrjd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1159&quot; height=&quot;545&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;만약 로컬에서 RDS에 접근하고자 한다면 Public access를 Yes로 해줘야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceVyNk/btsMOpeoEsV/YeIlvUt33EYvv2l3qviknK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceVyNk/btsMOpeoEsV/YeIlvUt33EYvv2l3qviknK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceVyNk/btsMOpeoEsV/YeIlvUt33EYvv2l3qviknK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceVyNk%2FbtsMOpeoEsV%2FYeIlvUt33EYvv2l3qviknK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;160&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;보안 그룹은 db-mysql-security라는 이름으로 새로 만들어줬다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6o5ci/btsMOYUTyTS/4suANHJ438PelKzP9G2YeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6o5ci/btsMOYUTyTS/4suANHJ438PelKzP9G2YeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6o5ci/btsMOYUTyTS/4suANHJ438PelKzP9G2YeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6o5ci%2FbtsMOYUTyTS%2F4suANHJ438PelKzP9G2YeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;251&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Database port가 3366으로 되어있는데, MySQL의 포트 번호가 3366이기 때문에 그대로 둔다. 포트 번호는 DB에 따라 바뀔 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfb9V9/btsMObHrPvS/5oKflMp6DrJ6PzpbDPYNYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfb9V9/btsMObHrPvS/5oKflMp6DrJ6PzpbDPYNYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfb9V9/btsMObHrPvS/5oKflMp6DrJ6PzpbDPYNYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfb9V9%2FbtsMObHrPvS%2F5oKflMp6DrJ6PzpbDPYNYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1091&quot; height=&quot;144&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Initial database name을 정하고 꼭 기억해둬야 한다. 그리고 자동 백업을 켜고 Backup retention period을 7로 설정해 7일 동안 자동 백업을 가지고 있도록 설정했다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JWxbs/btsMPrh9SjN/lmMt6fzwj3YvNXTa6BIMG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JWxbs/btsMPrh9SjN/lmMt6fzwj3YvNXTa6BIMG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JWxbs/btsMPrh9SjN/lmMt6fzwj3YvNXTa6BIMG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJWxbs%2FbtsMPrh9SjN%2FlmMt6fzwj3YvNXTa6BIMG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;643&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;643&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다 만들고 조금 기다리면 Avaliable이 뜨면서 성공적으로 RDS가 만들어진 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAViLB/btsMODjbPkv/xh6fdk23S2vkTCaTEi1iVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAViLB/btsMODjbPkv/xh6fdk23S2vkTCaTEi1iVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAViLB/btsMODjbPkv/xh6fdk23S2vkTCaTEi1iVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAViLB%2FbtsMODjbPkv%2Fxh6fdk23S2vkTCaTEi1iVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;264&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1 id=&quot;sqlectron-이용해-rds-사용하기&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;sqlectron 이용해 RDS 사용하기&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;먼저 아래 링크에서 sqlectron을 설치한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://sqlectron.github.io/&quot;&gt;sqlectron 설치하기&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;아까 기억해둔 db name과 password를 적는다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKcwmZ/btsMOhU4mjR/O1ghnmQxqApF3jqMFSZeT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKcwmZ/btsMOhU4mjR/O1ghnmQxqApF3jqMFSZeT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKcwmZ/btsMOhU4mjR/O1ghnmQxqApF3jqMFSZeT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKcwmZ%2FbtsMOhU4mjR%2FO1ghnmQxqApF3jqMFSZeT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;786&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여기서 Server Address는 RDS에 들어가면 나오는 Endpoint를 복사해 넣으면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;453&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJNHKr/btsMN7d0WWT/oRlv0cWGxT8hRt7Xw5NJR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJNHKr/btsMN7d0WWT/oRlv0cWGxT8hRt7Xw5NJR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJNHKr/btsMN7d0WWT/oRlv0cWGxT8hRt7Xw5NJR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJNHKr%2FbtsMN7d0WWT%2FoRlv0cWGxT8hRt7Xw5NJR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;453&quot; height=&quot;504&quot; data-origin-width=&quot;453&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;저장 후 Connect를 누르면 다음과 같이 뜬다. 이곳이 RDS에 있는 MySQL이다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beebkV/btsMNPdufMF/IvclL3pTnz5PTF6XI9QJYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beebkV/btsMNPdufMF/IvclL3pTnz5PTF6XI9QJYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beebkV/btsMNPdufMF/IvclL3pTnz5PTF6XI9QJYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeebkV%2FbtsMNPdufMF%2FIvclL3pTnz5PTF6XI9QJYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;786&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;간단한 SQL문을 작성해 Excute하니 user 테이블이 만들어진 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvBU1H/btsMOshRO6F/KiWOKHarCBydhXpCM07571/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvBU1H/btsMOshRO6F/KiWOKHarCBydhXpCM07571/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvBU1H/btsMOshRO6F/KiWOKHarCBydhXpCM07571/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvBU1H%2FbtsMOshRO6F%2FKiWOKHarCBydhXpCM07571%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;786&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;옆 user 테이블을 우클릭하면 다음과 같이 Insert, Update, Delete 등을 할 수 있다. 나는 Insert Statement를 눌러 데이터를 추가해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;248&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfprt5/btsMNQpV6KF/HKaGuw6oKrFnb6Tv8CrhQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfprt5/btsMNQpV6KF/HKaGuw6oKrFnb6Tv8CrhQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfprt5/btsMNQpV6KF/HKaGuw6oKrFnb6Tv8CrhQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfprt5%2FbtsMNQpV6KF%2FHKaGuw6oKrFnb6Tv8CrhQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;248&quot; height=&quot;299&quot; data-origin-width=&quot;248&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이 데이터를 넣고 Excute를 눌렀다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Un71J/btsMNPEynfk/0mLbXA3qMhwD5w4xip2cl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Un71J/btsMNPEynfk/0mLbXA3qMhwD5w4xip2cl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Un71J/btsMNPEynfk/0mLbXA3qMhwD5w4xip2cl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUn71J%2FbtsMNPEynfk%2F0mLbXA3qMhwD5w4xip2cl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;786&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;정상적으로 데이터가 들어간 모습을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgyDA4/btsMOwdoAkm/2G8Hbc5LXJkMzkw3YWSTe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgyDA4/btsMOwdoAkm/2G8Hbc5LXJkMzkw3YWSTe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgyDA4/btsMOwdoAkm/2G8Hbc5LXJkMzkw3YWSTe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgyDA4%2FbtsMOwdoAkm%2F2G8Hbc5LXJkMzkw3YWSTe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;786&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;RDS Monitoring에 들어와서 보면 변화가 생긴 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJLaIf/btsMOtgJEEL/GKOLvXCkZ4A2qDds13lmm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJLaIf/btsMOtgJEEL/GKOLvXCkZ4A2qDds13lmm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJLaIf/btsMOtgJEEL/GKOLvXCkZ4A2qDds13lmm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJLaIf%2FbtsMOtgJEEL%2FGKOLvXCkZ4A2qDds13lmm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;557&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>AWS</category>
      <category>RDS</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/379</guid>
      <comments>https://3uomlkh.tistory.com/379#entry379comment</comments>
      <pubDate>Tue, 18 Mar 2025 16:57:13 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security + JWT 사용하기</title>
      <link>https://3uomlkh.tistory.com/377</link>
      <description>&lt;h1 id=&quot;spring-security란&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Security란?&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;스프링 시큐리티(Spring Security)&lt;/b&gt;&amp;nbsp;: 스프링 기반 애플리케이션에 강력한 인증/인가 기능을 제공하는 보안 프레임워크.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Security와 JWT를 함께 사용하면&amp;nbsp;&lt;b&gt;Stateless&lt;/b&gt;하게 사용하기 때문에 간단하게 적용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Security 보안을 통과하려면 SecurityContext에&amp;nbsp;AbstractAuthenticationToken을 set해줘야 한다!&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;SecurityContext&lt;/b&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;보통 하나의&amp;nbsp;Authentication&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;사용자(Principal)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인증 방식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인증 상태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;사용자 권한 목록(GrantedAuthorities)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;잠시 코드로 살펴보면 아래와 같이 설정해주는 것을 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;// ... 중략

JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여기서&amp;nbsp;AbstractAuthenticationToken이 없는데? 라고 생각할수 있는데, 다음과 같이&amp;nbsp;JwtAuthenticationToken이&amp;nbsp;AbstractAuthenticationToken을 상속받고 있다. 따라서&amp;nbsp;JwtAuthenticationToken은 인증 객체로 활용될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public class JwtAuthenticationToken extends AbstractAuthenticationToken {

    private final AuthUser authUser;

    public JwtAuthenticationToken(AuthUser authUser) {
        super(authUser.getAuthorities());
        this.authUser = authUser;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return authUser;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&quot;spring-security-적용하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring Security 적용하기&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기존 Filter와 Argument Resolver를 사용하던 코드들을 Spring Security로 변경하고자 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;securityconfig&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;SecurityConfig&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Filter와 Argument Resolver를 각각 config에 등록했던 것처럼 SecurityConfig를 만든다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;SecurityConfig에 Filter를 등록한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기존에는 PasswordEncoder를 따로 만들어줘야 했지만, Spring Security에서는 기본적으로 BCryptPasswordEncoder를 제공해준다. 따라서 Bean으로 등록해 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true) // @Secured 권한 체크
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public PasswordEncoder passwordEncoder() { // 기본 제공
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -&amp;gt; session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .addFilterBefore(jwtAuthenticationFilter, SecurityContextHolderAwareRequestFilter.class)
                .formLogin(AbstractHttpConfigurer::disable)
                .anonymous(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .logout(AbstractHttpConfigurer::disable)
                .rememberMe(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -&amp;gt; auth
                        .requestMatchers(request -&amp;gt; request.getRequestURI().startsWith(&quot;/auth&quot;)).permitAll()
                        .requestMatchers(&quot;/test&quot;).hasAuthority(UserRole.Authority.ADMIN)
                        .requestMatchers(&quot;/open&quot;).permitAll()
                        .anyRequest().authenticated()
                )
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;코드를 차근차근 살펴보자. 다음과 같이 설정하면 SpringContextPersistenceFilter에서 매 요청마다 빈 SecurityContext를 설정해준다. 따라서 불필요하게 빈 SecurityContext를 생성하지 않고&amp;nbsp;SecurityContextHolder.getContext()를 통해 SecurityContext를 그냥 불러오기만 하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;.sessionManagement(session -&amp;gt; 
        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음 Filter들은 JWT를 사용하고 있는 지금은 필요없기 때문에 disable로 설정한다. 세션에서 활용되는 Filter이기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;.formLogin(AbstractHttpConfigurer::disable) // 폼 기반 로그인
.anonymous(AbstractHttpConfigurer::disable) // 익명 사용자 권한
.httpBasic(AbstractHttpConfigurer::disable) // 커스텀 Filter - 사용해도 되지만 여기서는 JwtAuthenticationFilter를 쓴다.
.logout(AbstractHttpConfigurer::disable) // 세션 정보를 지우는 요청(로그아웃) 관련
.rememberMe(AbstractHttpConfigurer::disable)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음은 요청에 대한 인증 및 권한을 설정하는 부분이다. 특정 URL 패턴에 대한 접근 제어를 정의하고, 나머지 요청은 인증된 사용자만 접근할 수 있도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;.authorizeHttpRequests(auth -&amp;gt; auth
        .requestMatchers(request -&amp;gt; request.getRequestURI().startsWith(&quot;/auth&quot;)).permitAll()
        .requestMatchers(&quot;/test&quot;).hasAuthority(UserRole.Authority.ADMIN)
        .requestMatchers(&quot;/open&quot;).permitAll()
        .anyRequest().authenticated()
)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;모든&amp;nbsp;/auth로 시작하는 요청은 인증 없이 접근 가능하도록 설정.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;/test&amp;nbsp;엔드포인트는&amp;nbsp;ADMIN&amp;nbsp;권한이 있는 사용자만 접근 가능하도록 설정.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;/open&amp;nbsp;엔드포인트는 모든 사용자에게 공개해 인증 없이 접근 가능하도록 설정.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위에서 지정한 URL 패턴을 제외한 모든 요청은 인증된 사용자만 접근 가능하도록 설정. 즉,&amp;nbsp;&lt;b&gt;AbstractAuthenticationToken이 set되어 있어야 통과가 가능&lt;/b&gt;하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해당 어노테이션을 설정하면 클래스나 메서드에&amp;nbsp;@Secured&amp;nbsp;어노테이션을 적용하여 역할 기반 접근 제어(Role-Based Access Control)를 설정할 수 있다. 서비스 클래스에서 특정 역할만 접근 가능하도록 설정하고 여러 개의 역할을 지정할 수 있다. 이 내용은 밑에서 다룬다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@EnableMethodSecurity(securedEnabled = true)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;jwtauthenticationfilter&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JwtAuthenticationFilter&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;FilterConfig에서는 Filter를 Bean으로 등록해주기 때문에 JwtFilter에서 따로 등록해줄 필요가 없다. 그러나 SecurityConfig에서는 Bean으로 등록해주지 않기 때문에 JwtAuthenticationFilter에서&amp;nbsp;@Component로 등록을 해줘야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기존 Filter와 거의 비슷하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Slf4j
@Component // Component로 등록해줘야 한다!
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(
            HttpServletRequest httpRequest,
            @NonNull HttpServletResponse httpResponse,
            @NonNull FilterChain chain
    ) throws ServletException, IOException {
        String authorizationHeader = httpRequest.getHeader(&quot;Authorization&quot;);

        if (authorizationHeader != null &amp;amp;&amp;amp; authorizationHeader.startsWith(&quot;Bearer &quot;)) {
            String jwt = jwtUtil.substringToken(authorizationHeader);
            try {
                Claims claims = jwtUtil.extractClaims(jwt);

                if (SecurityContextHolder.getContext().getAuthentication() == null) {
                    setAuthentication(claims);
                }
            } catch (SecurityException | MalformedJwtException e) {
                log.error(&quot;Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.&quot;, e);
                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, &quot;유효하지 않는 JWT 서명입니다.&quot;);
            } catch (ExpiredJwtException e) {
                log.error(&quot;Expired JWT token, 만료된 JWT token 입니다.&quot;, e);
                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, &quot;만료된 JWT 토큰입니다.&quot;);
            } catch (UnsupportedJwtException e) {
                log.error(&quot;Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.&quot;, e);
                httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, &quot;지원되지 않는 JWT 토큰입니다.&quot;);
            } catch (Exception e) {
                log.error(&quot;Internal server error&quot;, e);
                httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        }
        chain.doFilter(httpRequest, httpResponse);
    }

    private void setAuthentication(Claims claims) {
        Long userId = Long.valueOf(claims.getSubject());
        String email = claims.get(&quot;email&quot;, String.class);
        UserRole userRole = UserRole.of(claims.get(&quot;userRole&quot;, String.class));
        String nickName = claims.get(&quot;nickName&quot;, String.class);

        AuthUser authUser = new AuthUser(userId, email, userRole, nickName);
        JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위에서 두 번이나 언급한&amp;nbsp;&lt;b&gt;&quot;SecurityContext에&amp;nbsp;AbstractAuthenticationToken을 set해주는 과정&quot;&lt;/b&gt;이 바로 아래 코드다. JWT에서 사용자 정보를 추출한 후, Spring Security의 SecurityContext에 인증 정보를 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;private void setAuthentication(Claims claims) {
    Long userId = Long.valueOf(claims.getSubject());
    String email = claims.get(&quot;email&quot;, String.class);
    UserRole userRole = UserRole.of(claims.get(&quot;userRole&quot;, String.class));
    String nickName = claims.get(&quot;nickName&quot;, String.class);

    AuthUser authUser = new AuthUser(userId, email, userRole, nickName); // 인증된 사용자 정보 생성
    JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser); // 인증 토큰 생성
    SecurityContextHolder.getContext().setAuthentication(authenticationToken); // SecurityContext에 토큰 저장, 인가 통과를 위해
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JWT 인증이 통과되면 SecurityContextHolder에 Authentication이 저장되고, 컨트롤러에서&amp;nbsp;@AuthenticationPrincipal AuthUser authUser를 사용하면 AuthUser를 바로 가져올 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;jwtauthenticationtoken&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JwtAuthenticationToken&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;AbstractAuthenticationToken을 상속받는 클래스로, Spring Security에서 JWT 기반 인증을 처리하기 위한 Authentication 객체다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기존의 AuthuserArgumentResolver를 대체한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;@Auth&amp;nbsp;대신&amp;nbsp;@AuthenticationPrincipal을 사용한다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public class JwtAuthenticationToken extends AbstractAuthenticationToken {

    private final AuthUser authUser;

    public JwtAuthenticationToken(AuthUser authUser) {
        super(authUser.getAuthorities()); // Authentication 객체에 사용자 권한 저장
        this.authUser = authUser;
        setAuthenticated(true); // 인증 완료된 사용자로 표시
    }

    @Override
    public Object getCredentials() { // JWT 기반 인증에서는 비밀번호가 필요 없으므로 null 반환
        return null;
    }

    @Override
    public Object getPrincipal() {
        return authUser;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;@getPrincipal()&amp;nbsp;메서드로 컨트롤러 파라미터에서&amp;nbsp;@AuthenticationPrincipal을 이용해 어떤 인증 객체를 받을지 설정한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Override
public Object getPrincipal() {
    return authUser;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;userrole&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;UserRole&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기존의 UserRole enum 클래스는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;crystal&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public enum UserRole {
    ADMIN, USER;

    public static UserRole of(String role) {
        return Arrays.stream(UserRole.values())
                .filter(r -&amp;gt; r.name().equalsIgnoreCase(role))
                .findFirst()
                .orElseThrow(() -&amp;gt; new InvalidRequestException(&quot;유효하지 않은 UerRole&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;수정된 코드는 다음과 같다. Spring Security에서 제공하는 권한 기능을 사용하려면&amp;nbsp;&lt;b&gt;반드시 prefix로&amp;nbsp;ROLE_을 붙여야 한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Authority 클래스는&amp;nbsp;ROLE_USER과&amp;nbsp;ROLE_ADMIN을 하드코딩하지 않고&amp;nbsp;UserRole.Authority.USER와 같이 사용할 수 있도록 정의하는 역할을 한다.&amp;nbsp;&lt;b&gt;@Secured&amp;nbsp;안에 enum을 바로 넣지 못하기 때문에&lt;/b&gt;&amp;nbsp;이런 클래스를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
public enum UserRole {
    ROLE_USER(Authority.USER),
    ROLE_ADMIN(Authority.ADMIN);

    private final String userRole;

    public static UserRole of(String role) {
        return Arrays.stream(UserRole.values())
                .filter(r -&amp;gt; r.name().equalsIgnoreCase(role))
                .findFirst()
                .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;유효하지 않은 UserRole&quot;));
    }

    public static class Authority {
        public static final String USER = &quot;ROLE_USER&quot;;
        public static final String ADMIN = &quot;ROLE_ADMIN&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음은&amp;nbsp;@Secured&amp;nbsp;안에 enum을 넣었을 때 호환되지 않는 타입이라고 에러가 발생하는 모습이다.&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;1280&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pymYR/btsMLPqGtNn/qDEyVgy6rs2z5kjGTkZnek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pymYR/btsMLPqGtNn/qDEyVgy6rs2z5kjGTkZnek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pymYR/btsMLPqGtNn/qDEyVgy6rs2z5kjGTkZnek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpymYR%2FbtsMLPqGtNn%2FqDEyVgy6rs2z5kjGTkZnek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;217&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;authuser&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;AuthUser&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기존의 AuthUser는 UserRole을 필드로 가지고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Getter
public class AuthUser {

    private final Long id;
    private final String email;
    private final UserRole userRole;
    private final String nickName;

    public AuthUser(Long id, String email, UserRole userRole, String nickName) {
        this.id = id;
        this.email = email;
        this.userRole = userRole;
        this.nickName = nickName;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그러나 Spring Security에서는 다음과 같이 Collection 타입의 필드인 authorities로 대체되었다. 권한이 여러 개일 수도 있는 상황을 커버하기 위해 이런 타입으로 받는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Getter
public class AuthUser {

    private final Long id;
    private final String email;
    private final String nickName;
    private final Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities;

    public AuthUser(Long id, String email, UserRole userRole, String nickName) {
        this.id = id;
        this.email = email;
        this.authorities = List.of(new SimpleGrantedAuthority(userRole.name()));
        this.nickName = nickName;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;secured-사용하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;@Secured 사용하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;@EnableMethodSecurity(securedEnabled = true)를 설정하면&amp;nbsp;@Secured를 사용할 수 있다. 그리고 다음과 같이&amp;nbsp;@Secured&amp;nbsp;어노테이션을 사용하면&amp;nbsp;&lt;b&gt;이 API는 권한이 ADMIN인 사용자만 사용이 가능하다&lt;/b&gt;는 것을 직관적으로 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Secured(UserRole.Authority.ADMIN)
@PatchMapping(&quot;/admin/users/{userId}&quot;)
public void changeUserRole(
        @PathVariable long userId,
        @RequestBody UserRoleChangeRequest userRoleChangeRequest) {
    userAdminService.changeUserRole(userId, userRoleChangeRequest);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>jwt</category>
      <category>Spring</category>
      <category>springsecurity</category>
      <category>스프링시큐리티</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/377</guid>
      <comments>https://3uomlkh.tistory.com/377#entry377comment</comments>
      <pubDate>Sat, 15 Mar 2025 00:10:08 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring Security 도입 후 NullPointException 해결(@AuthenticationPrincipal)</title>
      <link>https://3uomlkh.tistory.com/378</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;어제 Spring Security 도입을 마무리 하고, 다음 단계를 진행하다가 일정을 생성하는 부분에서 다음과 같이 NPE가 발생했다.&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;1280&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sQyU9/btsMI43zurA/bbDwlifvN210EdrY38EtIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sQyU9/btsMI43zurA/bbDwlifvN210EdrY38EtIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sQyU9/btsMI43zurA/bbDwlifvN210EdrY38EtIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsQyU9%2FbtsMI43zurA%2FbbDwlifvN210EdrY38EtIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;522&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Postman에서는 403 Forbidden 에러가 난 상황이다.&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;912&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bStMmU/btsMJAUXWe2/ZRH9Zjy0tEUKMkiM2wjYwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bStMmU/btsMJAUXWe2/ZRH9Zjy0tEUKMkiM2wjYwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bStMmU/btsMJAUXWe2/ZRH9Zjy0tEUKMkiM2wjYwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbStMmU%2FbtsMJAUXWe2%2FZRH9Zjy0tEUKMkiM2wjYwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;912&quot; height=&quot;746&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&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-origin-width=&quot;1280&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhFxph/btsMKG7TyhD/qh0Ygx4kdkiAYT0zdpn3y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhFxph/btsMKG7TyhD/qh0Ygx4kdkiAYT0zdpn3y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhFxph/btsMKG7TyhD/qh0Ygx4kdkiAYT0zdpn3y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhFxph%2FbtsMKG7TyhD%2Fqh0Ygx4kdkiAYT0zdpn3y1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;99&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;99&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;왜 userRole이 null인가...  DB에도 다 들어있는데 왜 null이지...?&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;1256&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OIkZ8/btsMKaVPDwE/yuOCGDzpjNoTo4DNUAW6Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OIkZ8/btsMKaVPDwE/yuOCGDzpjNoTo4DNUAW6Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OIkZ8/btsMKaVPDwE/yuOCGDzpjNoTo4DNUAW6Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOIkZ8%2FbtsMKaVPDwE%2FyuOCGDzpjNoTo4DNUAW6Kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1256&quot; height=&quot;295&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이 상황을 믿을 수가 없어 생성자에서 userRole이 null일 때 에러를 던지도록 해 다시 한 번 확인해봤다.&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;1280&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh0I03/btsMLsHWHlc/xjaGUWBgpHyXy5xo7AhajK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh0I03/btsMLsHWHlc/xjaGUWBgpHyXy5xo7AhajK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh0I03/btsMLsHWHlc/xjaGUWBgpHyXy5xo7AhajK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh0I03%2FbtsMLsHWHlc%2FxjaGUWBgpHyXy5xo7AhajK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;396&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;null이 맞다. 그렇다면 왜일까?&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;1280&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNaRa6/btsMK8CUvOo/m12ZKzwfc3rz57gb50vklK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNaRa6/btsMK8CUvOo/m12ZKzwfc3rz57gb50vklK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNaRa6/btsMK8CUvOo/m12ZKzwfc3rz57gb50vklK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNaRa6%2FbtsMK8CUvOo%2Fm12ZKzwfc3rz57gb50vklK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;205&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;205&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;검색의 늪에 빠져있다가 여러 상황을 가정하고 확인해보았다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;s&gt;JwtUtil과 JwtAuthenticationFilter에서userRole을 다른 단어로 입력한 경우&lt;/s&gt;&amp;nbsp;&amp;rarr; 둘 다&amp;nbsp;userRole로 같음.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;s&gt;DB에 저장되지 않은 경우&lt;/s&gt;&amp;nbsp;&amp;rarr; 아주 잘 저장되어있음.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;s&gt;JWT 생성 시&amp;nbsp;userRole이 포함되지 않은 경우&lt;/s&gt;&amp;nbsp;&amp;rarr; 이 부분은 원래 있던 코드라 그럴 리 없음.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;AuthUser를 생성하는 부분에서 null이 들어가고 있는 것 같은데, AuthUser를 만들고 있는 부분을 찾아보자 &amp;rarr; 이 흐름이 맞았음!&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JwtAuthenticationFilter에서&amp;nbsp;setAuthentication()을 보면 다음과 같다. 여기서 해당 메서드는 JWT 토큰을 검증한 후, Spring Security의 SecurityContext에 인증된 사용자 정보를 저장하는 역할을 한다. 여기서&amp;nbsp;JwtAuthenticationToken&amp;nbsp;부분을 보면 Spring Security에서 사용할 Authentication 객체를 생성한다.&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;1280&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GmIQw/btsMKBMmPv1/PB0GIoENFNStLnZ08uMnF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GmIQw/btsMKBMmPv1/PB0GIoENFNStLnZ08uMnF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GmIQw/btsMKBMmPv1/PB0GIoENFNStLnZ08uMnF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGmIQw%2FbtsMKBMmPv1%2FPB0GIoENFNStLnZ08uMnF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;468&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JwtAuthenticationToken&amp;nbsp;클래스로 가서 다시 한 번 살펴보니 사용자의 권한(userRole)을 설정하고, 인증된 사용자로 set한다. 그리고 맨 아래&amp;nbsp;getPrincipal()&amp;nbsp;메서드를 통해 현재 로그인한 사용자의 정보를 반환한다. 여기서 이&amp;nbsp;getPrincipal()&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;1280&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsDL0j/btsMKT0hpFE/kgVNnm4O3yki6YcokHkVB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsDL0j/btsMKT0hpFE/kgVNnm4O3yki6YcokHkVB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsDL0j/btsMKT0hpFE/kgVNnm4O3yki6YcokHkVB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsDL0j%2FbtsMKT0hpFE%2FkgVNnm4O3yki6YcokHkVB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;824&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 나는 이 부분을 이용해야 한다는 생각이 들어&amp;nbsp;TodoController의&amp;nbsp;saveTodo()&amp;nbsp;메서드에서&amp;nbsp;@Auth&amp;nbsp;어노테이션을 주석 처리한 후, 직접&amp;nbsp;getPrincipal()&amp;nbsp;메서드를 불러&amp;nbsp;authUser에 넣고 테스트 해봤다.&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;1280&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kVSnH/btsMKEWzlrk/Ybx4lpCBYfEQx3oh7EGOiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kVSnH/btsMKEWzlrk/Ybx4lpCBYfEQx3oh7EGOiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kVSnH/btsMKEWzlrk/Ybx4lpCBYfEQx3oh7EGOiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkVSnH%2FbtsMKEWzlrk%2FYbx4lpCBYfEQx3oh7EGOiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;435&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;드디어 제대로 일정이 생성되었다! 하지만&amp;nbsp;@Auth를 이용해 바꿔야 했는데, 이 부분에서 내가 어제 실수를 했다는 것을 알게되었다.&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;1280&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csIv3h/btsMKvk2KbR/1Ejxmj0oMTK9di08T2VlwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csIv3h/btsMKvk2KbR/1Ejxmj0oMTK9di08T2VlwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csIv3h/btsMKvk2KbR/1Ejxmj0oMTK9di08T2VlwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsIv3h%2FbtsMKvk2KbR%2F1Ejxmj0oMTK9di08T2VlwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;58&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;바로 파일 정리를 하다가 생각없이&amp;nbsp;AuthUserArgumentResolver와&amp;nbsp;WebConfig를 삭제해버린 것이다.... 이 부분은 삭제하지 말고 Spring Security로 변경했어야 했는데, 필요없다고 생각해서 지웠다. 차라리 지우지 않았더라면 더 빨리 해결할 수 있었을 텐데 말이다. 정확한 이해 없이 적용하려고만 해서 이런 일이 벌어진 것 같다. &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;622&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhi8Ya/btsMKSG4lYm/xNdVwRCikaoCvto5vH1VsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhi8Ya/btsMKSG4lYm/xNdVwRCikaoCvto5vH1VsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhi8Ya/btsMKSG4lYm/xNdVwRCikaoCvto5vH1VsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdhi8Ya%2FbtsMKSG4lYm%2FxNdVwRCikaoCvto5vH1VsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;550&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 다시 해당 파일을 만든 후,&amp;nbsp;AuthUserArgumentResolver의&amp;nbsp;resolveArgument()메서드를 다음과 같이 수정했다. 이전에는 값을 request에서 하나씩 가져와 새로운 AuthUser 객체를 만들어 반환했다.&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;1280&quot; data-origin-height=&quot;729&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCBfvN/btsMKCEtVUx/8vhfi2apaiRHTUa9WGqS8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCBfvN/btsMKCEtVUx/8vhfi2apaiRHTUa9WGqS8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCBfvN/btsMKCEtVUx/8vhfi2apaiRHTUa9WGqS8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCBfvN%2FbtsMKCEtVUx%2F8vhfi2apaiRHTUa9WGqS8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;729&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;729&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;수정 후, 현재 인증된 사용자의 정보를 가져와서(SecurityContextHolder.getContext().getAuthentication()) Principal을 꺼내(getPrincipal()) AuthUser로 반환한다. 이렇게 반환된 AuthUser는&amp;nbsp;TodoController의&amp;nbsp;authUser&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;1280&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biFDC9/btsMJo1xVUl/CS1okGG6k6DkWhrvpUM6G1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biFDC9/btsMJo1xVUl/CS1okGG6k6DkWhrvpUM6G1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biFDC9/btsMJo1xVUl/CS1okGG6k6DkWhrvpUM6G1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiFDC9%2FbtsMJo1xVUl%2FCS1okGG6k6DkWhrvpUM6G1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;370&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1280&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSHaQT/btsMI4P0SZl/gcsjy9cgz6cKOA3Ppvn2A1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSHaQT/btsMI4P0SZl/gcsjy9cgz6cKOA3Ppvn2A1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSHaQT/btsMI4P0SZl/gcsjy9cgz6cKOA3Ppvn2A1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSHaQT%2FbtsMI4P0SZl%2Fgcsjy9cgz6cKOA3Ppvn2A1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;340&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Postman에서도 잘 테스트 된다.&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;912&quot; data-origin-height=&quot;751&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/betP3Q/btsMJdeRdj1/KHRZmlmYYZXFSNq5XPPnKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/betP3Q/btsMJdeRdj1/KHRZmlmYYZXFSNq5XPPnKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/betP3Q/btsMJdeRdj1/KHRZmlmYYZXFSNq5XPPnKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbetP3Q%2FbtsMJdeRdj1%2FKHRZmlmYYZXFSNq5XPPnKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;912&quot; height=&quot;751&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;751&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;자, 이렇게 겨우겨우 에러를 해결했지만 이 방법을 아래 어노테이션 하나로 해결할 수 있었다.&lt;/b&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;@AuthenticationPrincipal&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1280&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mML4a/btsMKyB3ADZ/4wuNPMtvwpHfsRKfjb31Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mML4a/btsMKyB3ADZ/4wuNPMtvwpHfsRKfjb31Xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mML4a/btsMKyB3ADZ/4wuNPMtvwpHfsRKfjb31Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmML4a%2FbtsMKyB3ADZ%2F4wuNPMtvwpHfsRKfjb31Xk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;349&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;349&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;...나 오늘 뭐한거지...?  팀원 분이 마지막 스크럼때 알려주셔서 알았다. 강의를 더 자세히 봐야한다는 교훈을 얻었다. 강의 다시 봐야지...&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/378</guid>
      <comments>https://3uomlkh.tistory.com/378#entry378comment</comments>
      <pubDate>Fri, 14 Mar 2025 10:59:33 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] application.yml에 시크릿 키 넣지 말아주세요...(.env 파일 만들고 설정하기)</title>
      <link>https://3uomlkh.tistory.com/376</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;먼저 resource 패키지 아래에 .env 파일을 만들고 올라가면 안되는 정보를 넣어준다.&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;989&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yw7TH/btsMHfbd6sd/NC8A1jkl9k4HmKJBi6IeQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yw7TH/btsMHfbd6sd/NC8A1jkl9k4HmKJBi6IeQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yw7TH/btsMHfbd6sd/NC8A1jkl9k4HmKJBi6IeQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYw7TH%2FbtsMHfbd6sd%2FNC8A1jkl9k4HmKJBi6IeQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;989&quot; height=&quot;152&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그리고 인텔리제이 오른쪽 상단에 있는 점 세개를 누른 후 Edit...에 들어간다.&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;532&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tmvsB/btsMFLBzD4Z/suaQx1NP8C2VwEgsXyaKp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tmvsB/btsMFLBzD4Z/suaQx1NP8C2VwEgsXyaKp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tmvsB/btsMFLBzD4Z/suaQx1NP8C2VwEgsXyaKp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtmvsB%2FbtsMFLBzD4Z%2FsuaQx1NP8C2VwEgsXyaKp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;532&quot; height=&quot;170&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;170&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;현재 내가 이미 환경 변수를 설정해놓은 상태지만, Environment variables가 없다면 Build and Run 옆에 있는 Modify options를 누른다.&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;802&quot; data-origin-height=&quot;693&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/carX4x/btsMGfh6VaL/EeFWGb0rkCyJTNHLQKwcVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/carX4x/btsMGfh6VaL/EeFWGb0rkCyJTNHLQKwcVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/carX4x/btsMGfh6VaL/EeFWGb0rkCyJTNHLQKwcVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcarX4x%2FbtsMGfh6VaL%2FEeFWGb0rkCyJTNHLQKwcVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;802&quot; height=&quot;693&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;693&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그럼 이렇게 옵션들을 추가할 수 있는데, Environment variables를 추가해준다.&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;413&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwG0nv/btsMHgt4bJP/iKQcsdknXpVeQ4XkkaK6ZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwG0nv/btsMHgt4bJP/iKQcsdknXpVeQ4XkkaK6ZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwG0nv/btsMHgt4bJP/iKQcsdknXpVeQ4XkkaK6ZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwG0nv%2FbtsMHgt4bJP%2FiKQcsdknXpVeQ4XkkaK6ZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;413&quot; height=&quot;774&quot; data-origin-width=&quot;413&quot; data-origin-height=&quot;774&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이제 이 환경 변수 경로를 설정해야한다. 경로는 .env 파일이 있는 경로로 설정하면 된다. 그리고 .env 파일 역시 깃허브에 올라가면 안되므로 해당 파일을 gitignore에 추가한다.&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;332&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9aZmd/btsMFHsTfDj/Wlou6cI7QR2KcTaHR1nG7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9aZmd/btsMFHsTfDj/Wlou6cI7QR2KcTaHR1nG7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9aZmd/btsMFHsTfDj/Wlou6cI7QR2KcTaHR1nG7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9aZmd%2FbtsMFHsTfDj%2FWlou6cI7QR2KcTaHR1nG7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;332&quot; height=&quot;178&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;178&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그러면 application.yml 파일에 직접 민감한 정보를 넣을 필요 없이 환경 변수 설정을 할 수 있다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>환경변수</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/376</guid>
      <comments>https://3uomlkh.tistory.com/376#entry376comment</comments>
      <pubDate>Mon, 10 Mar 2025 21:47:24 +0900</pubDate>
    </item>
    <item>
      <title>프로젝트를 시작할 때 생각해볼 것들  </title>
      <link>https://3uomlkh.tistory.com/375</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333;&quot;&gt;&lt;i&gt;이번 프로젝트를 끝내고, 지난 프로젝트에서 아쉬웠던 점을 정리해봤다. ️&lt;/i&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333;&quot;&gt;&lt;i&gt;그런 점을 이번 프로젝트에서 꽤 많이 개선한 것 같고, &lt;/i&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333;&quot;&gt;&lt;i&gt;이번 프로젝트에서의 아쉬운 점 역시 다음 프로젝트에서 개선할 수 있다면 좋을 것 같다!&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;지난 프로젝트에서는 git에 익숙하지 않은 팀원분들이 많았다. 그래서인지 사실 내가 맡은 기능을 개발하는 시간보다 git 관련 문제를 해결하는데 더 많은 시간을 쓴 것 같다. 우리팀에서 일어난 문제는 다음과 같았다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;merge가 되면 develop에서 pull을 받은 후, 자신의 브랜치에서 작업을 해야한다. 그러나 중간에 확인해보니 팀원 모두가 pull을 받지 않고 작업을 하고 있다는 사실을 알았다. 자신의 브랜치에서는 자신이 맡은 기능만을 개발해야한다고 알고 있었던 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&amp;nbsp;자신의 브랜치에서 develop 브랜치로 merge를 해야하는데, 다른 팀원이 실수로 main 브랜치로 PR을 한 후 합쳐버렸다. 이런 상황을 미리 막기 위해 아예 develop 브랜치를 default 브랜치로 정했어야 했는데 아쉬운 부분이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;PR 제목 컨벤션이나 내용 템플릿을 정하지 않아 통일성이 없는 PR들이 생겨나서 신경쓰였다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&amp;nbsp;커밋 메세지가 깨져서 알아보기 힘든 경우가 많았다. 보통 터미널에서 커밋 메세지를 작성하면 그런 문제가 생기는 것 같다. 나는 주로 커밋 메세지는 인텔리제이가 제공하는 창을 이용해 적어서 몰랐다. 지금 찾아보니 &lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@sians0209/Git-Commit-%EB%A9%94%EC%8B%9C%EC%A7%80-%ED%95%9C%EA%B8%80-%EA%B9%A8%EC%A7%90-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;인코딩 세팅을 변경&lt;/a&gt; 해주면 해결되는 것 같다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;내가 알고 있는 내용이더라도 다른 사람은 모를 수 있고, 혹은 다른 스타일을 가지고 있을 수 있기 때문에 정말 사소한 부분까지도 공유하고 이야기를 나눠보는 것이 중요하다고 느꼈다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;프로젝트 내에서 거의 모든 도메인에서 사용되는 예외 처리나 리소스 패키지 안에 있는 파일 등을 잘 관리해야겠다고 느꼈다. 같은 기능을 파일이 여러 개 올라오거나, 올라오지 말아야 할 파일이 올라왔기 때문이다. 특히 application.properties와 같은 설정 파일은 .env 파일을 만들어 환경 변수를 관리하는 작업이 필요하다. 또한 공용으로 사용되는 파일은 미리 global(혹은 common) 패키지를 만들어 한 사람이 작성해 올리도록 하는 게 낫지 않을까 싶다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;commit &amp;amp; code convention을 자세히 정해놓자&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;간단한 커밋 컨벤션만 정하고 프로젝트에 들어갔기 때문에 통일성이 없는 부분이 상당히 많았다. 이건 발표 때 튜터님이 다른 팀에 지적한 내용인데, 우리팀에도 해당된다고 생각했다. 미리 파일명, 메서드명, 변수명 컨벤션을 정하고 시작했다면 좋았을 것 같다. 예를들어 어떤 조회 메서드의 이름을 findXXX(), getXXX() 둘 중 어떤 것을 쓸지, 각 엔티티에서 변수명은 어떤 식으로 쓸지 등을 정해놓으면 좋을 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;아주 긴 주석을 마주하면 읽을 용기가 없어진다. 내가 생각하는 좋은 코드는 주석 없이도 잘 이해되는 코드지만, 주석을 달아야 한다면 정말 필요한 부분 한 두줄만 있으면 될 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;남이 쓴 코드를 읽는 건 정말 피곤한 일이다. 하지만 코드를 읽고, 이해하고, 잘못된 점이 있는지 검토하고, 리뷰를 달기 전 내가 알고 있는 지식이 맞는지 다시 한 번 검색하는 작업은 나를 더 성장시킨다. 지난 프로젝트에서 코드 리뷰를 하고 싶었지만 시간에 쫓기듯 개발해서 하지 못했다. 하지만 코드 리뷰를 했다면 다들 더 좋은 코드를 작성할 수 있는 힘을 기를 수 있지 않았을까? 이번 프로젝트에서 코드 리뷰를 하면서 코드 리뷰의 중요성을 다시 한 번 깨달았다. 그리고 스스로 실력이 부족하다는 생각에 코드리뷰를 잘 하지 않는 경우가 있는데, 오히려 다른 사람들의 코드를 보먀 왜 이렇게 짰는지 의견을 나눠보고 새로 알게 된 점을 따로 공부해보면 더 실력이 좋아질 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여기서 말하는 자세한 피드백은 코드 리뷰를 말한다. API&amp;nbsp; URL이나 진행 상황에 대한 피드백 이외에 핵심 기능에 대한 코드 리뷰를 받았다면 좋았을 것 같다. 이 부분은 이번 프로젝트 후 느낀 점이다. 이번 프로젝트에서 튜터님의 상세한 코드 리뷰를 듣고 어떤 방향으로 리팩토링 해볼 수 있을지에 대한 힌트를 얻었기 때문이다.&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;</description>
      <category>회고</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/375</guid>
      <comments>https://3uomlkh.tistory.com/375#entry375comment</comments>
      <pubDate>Sat, 8 Mar 2025 19:29:54 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] enum abstract method로 중복을 해결해보자</title>
      <link>https://3uomlkh.tistory.com/374</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 이번 심화 프로젝트를 진행하면서, 주문 상태가 늘어날수록 API와 로직이 같이 늘어나는 구조의 한계를 마주했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; ️ 변경 전 코드&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;우선 &lt;b&gt;OwnerOrderController&lt;/b&gt;는 가게 사장님이 들어온 주문을 수락 및 거절하는 API를 가지고 있다. 수락이 되었다면 음식조리중, 배달중, 배달완료 순으로 흐를 수 있도록 해야한다. 다음 코드를 보면, 각각의 상태에 따라 API가 다 따로 만들어져있다. 처음에는 상태가 되돌아 갈 수 없도록 하는 것에만 집중해서 예외 처리만 하고 끝냈다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741330106495&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api&quot;)
public class OwnerOrderController {

    private final OwnerOrderService ownerOrderService;

    @Order
    @PatchMapping(&quot;/v1/orders/{orderId}/accept&quot;)
    public ResponseEntity&amp;lt;OrderStatusResponseDto&amp;gt; acceptOrder(
            @Auth AuthUser owner,
            @PathVariable Long orderId
    ) {
        return ResponseEntity.ok(ownerOrderService.acceptOrder(owner.getId(), orderId));
    }

    @Order
    @PatchMapping(&quot;/v1/orders/{orderId}/reject&quot;)
    public ResponseEntity&amp;lt;OrderStatusResponseDto&amp;gt; rejectOrder(
            @Auth AuthUser owner,
            @PathVariable Long orderId
    ) {
        return ResponseEntity.ok(ownerOrderService.rejectOrder(owner.getId(), orderId));
    }

    @Order
    @PatchMapping(&quot;/v1/orders/{orderId}/cook&quot;)
    public ResponseEntity&amp;lt;OrderStatusResponseDto&amp;gt; startCooking(
            @Auth AuthUser owner,
            @PathVariable Long orderId
    ) {
        return ResponseEntity.ok(ownerOrderService.startCooking(owner.getId(), orderId));
    }

    @Order
    @PatchMapping(&quot;/v1/orders/{orderId}/deliver&quot;)
    public ResponseEntity&amp;lt;OrderStatusResponseDto&amp;gt; startDelivery(
            @Auth AuthUser owner,
            @PathVariable Long orderId
    ) {
        return ResponseEntity.ok(ownerOrderService.startDelivery(owner.getId(), orderId));
    }

    @Order
    @PatchMapping(&quot;/v1/orders/{orderId}/complete&quot;)
    public ResponseEntity&amp;lt;OrderStatusResponseDto&amp;gt; completeDelivery(
            @Auth AuthUser owner,
            @PathVariable Long orderId
    ) {
        return ResponseEntity.ok(ownerOrderService.completeDelivery(owner.getId(), orderId));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이런 경우는 문제점이 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt;만약, API가 점점 늘어나서 몇 백개, 몇 만개가 된다면?&lt;/i&gt;&lt;/u&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;물론 주문하고 배달하기까지의 과정이 그렇게까지 많은 상태로 쪼개지긴 힘들겠지만, 어떤 상태가 추가된다면 또 새로운 API를 추가해야한다. 그리고 다음과 같이 상태 하나하나 따로 호출하게 된다.&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;257&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dl21ld/btsMEmWIoT5/y3rqEB2KknmVyiF8fe5lw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dl21ld/btsMEmWIoT5/y3rqEB2KknmVyiF8fe5lw0/img.png&quot; data-alt=&quot;음... &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dl21ld/btsMEmWIoT5/y3rqEB2KknmVyiF8fe5lw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdl21ld%2FbtsMEmWIoT5%2Fy3rqEB2KknmVyiF8fe5lw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;257&quot; height=&quot;179&quot; data-origin-width=&quot;257&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;음... &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1741349717653&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PATCH api/v1/orders/1/reject&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1741349748927&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PATCH api/v1/orders/1/cook&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1741349767018&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PATCH api/v1/orders/1/deliver&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;...이렇게 API URL만 적다가 끝나버릴수도...&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그리고 &lt;b&gt;OwnerOrderService&lt;/b&gt;를 보면 한 눈에 봐도 중복되는 코드가 아주 많다는 것을 볼 수 있다. 중복되는 코드가 눈에 보이면 빠르게 하나로 합칠 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741330068103&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 OwnerOrderService {

    private final UserOrderService userOrderService;

    @Transactional
    public OrderStatusResponseDto acceptOrder(Long ownerId, Long orderId) {
        UserOrderEntity order = userOrderService.getOrderById(orderId);

        if (!Objects.equals(ownerId, order.getStore().getUser().getId())) {
            throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
        }

        if (!OrderStatus.PENDING.equals(order.getOrderStatus())) {
            throw new InvalidRequestException(ErrorOrderMessage.CANNOT_CANCEL_ORDER.getMessage());
        }

        order.updateOrderStatus(OrderStatus.ACCEPTED);

        return OrderStatusResponseDto.from(order);
    }

    @Transactional
    public OrderStatusResponseDto rejectOrder(Long ownerId, Long orderId) {
        UserOrderEntity order = userOrderService.getOrderById(orderId);

        if (!Objects.equals(ownerId, order.getStore().getUser().getId())) {
            throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
        }

        if (!OrderStatus.PENDING.equals(order.getOrderStatus())) {
            throw new InvalidRequestException(ErrorOrderMessage.CANNOT_CANCEL_ORDER.getMessage());
        }

        order.updateOrderStatus(OrderStatus.REJECTED);

        return OrderStatusResponseDto.from(order);
    }

    @Transactional
    public OrderStatusResponseDto startCooking(Long ownerId, Long orderId) {
        UserOrderEntity order = userOrderService.getOrderById(orderId);

        if (!Objects.equals(ownerId, order.getStore().getUser().getId())) {
            throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
        }

        if (OrderStatus.DELIVERING.equals(order.getOrderStatus())
        || OrderStatus.COMPLETED.equals(order.getOrderStatus())) {
            throw new InvalidRequestException(ErrorOrderMessage.ALREADY_DELIVERING_OR_COMPLETED.getMessage());
        }

        if (!OrderStatus.ACCEPTED.equals(order.getOrderStatus())) {
            throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_ACCEPTED_BEFORE_COOKING.getMessage());
        }

        order.updateOrderStatus(OrderStatus.COOKING);

        return OrderStatusResponseDto.from(order);
    }

    @Transactional
    public OrderStatusResponseDto startDelivery(Long ownerId, Long orderId) {
        UserOrderEntity order = userOrderService.getOrderById(orderId);

        if (!ObjectUtils.nullSafeEquals(ownerId, order.getStore().getUser().getId())) {
            throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
        }

        if (OrderStatus.DELIVERING.equals(order.getOrderStatus())
                || OrderStatus.COMPLETED.equals(order.getOrderStatus())) {
            throw new InvalidRequestException(ErrorOrderMessage.ALREADY_DELIVERING_OR_COMPLETED.getMessage());
        }

        if (!OrderStatus.COOKING.equals(order.getOrderStatus())) {
            throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_COOKING_BEFORE_DELIVERY.getMessage());
        }

        order.updateOrderStatus(OrderStatus.DELIVERING);

        return OrderStatusResponseDto.from(order);
    }

    @Transactional
    public OrderStatusResponseDto completeDelivery(Long ownerId, Long orderId) {
        UserOrderEntity order = userOrderService.getOrderById(orderId);

        if (!ObjectUtils.nullSafeEquals(ownerId, order.getStore().getUser().getId())) {
            throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
        }

        if (OrderStatus.COMPLETED.equals(order.getOrderStatus())) {
            throw new InvalidRequestException(ErrorOrderMessage.ALREADY_COMPLETED.getMessage());
        }

        if (!OrderStatus.DELIVERING.equals(order.getOrderStatus())) {
            throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_DELIVERING_BEFORE_COMPLETE.getMessage());
        }

        order.updateOrderStatus(OrderStatus.COMPLETED);

        return OrderStatusResponseDto.from(order);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;▪️order.updateOrderStatus()로 주문 상태를 변경하는 코드&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;▪️ OrderStatusResponseDto을 반환하는 코드&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;변경 전에는 다음과 같이 enum 클래스에서 상태명만 가져와 사용하고 있다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741335817099&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
public enum OrderStatus {

    PENDING(&quot;주문 확인전&quot;),
    CANCELED(&quot;주문 취소&quot;),
    ACCEPTED(&quot;주문 접수&quot;),
    REJECTED(&quot;주문 거절&quot;),
    COOKING(&quot;조리중&quot;),
    DELIVERING(&quot;배달중&quot;),
    COMPLETED(&quot;배달 완료&quot;);

    private final String description;

}&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이제 리팩토링을 해보자!&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✨ 리팩토링한 코드&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;튜터님의 피드백을 정리해보면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;✔️ 중복되는 코드가 너무 많다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;✔️ 상태가 추가되면 API를 계속 추가해야하고, 관리가 어려워진다는 문제가 생긴다. 주문 상태를 파라미터마다 받는 것은 어떨까?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;✔️ 각 상태마다 switch문으로 분기할 수 있을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;✔️ 각 상태별 예외처리를 메서드로 분리해 주문 상태에 따른 처리 로직만 남기면 코드를 간소화할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; ✔️ 현재 enum 클래스로 상태를 관리하고 있으므로, enum 내에 추상 메서드를 이용해 그 안에서 예외처리를 해볼 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;먼저, 주문상태를 &lt;b&gt;@RequestParam&lt;/b&gt;으로 받도록 변경했다. 다음과 같이 단 하나의 API만 남기고 모두 지웠다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741335943580&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api&quot;)
public class OwnerOrderController {

    private final OwnerOrderService ownerOrderService;

    @Order
    @PatchMapping(&quot;/v1/orders/{orderId}&quot;)
    public ResponseEntity&amp;lt;OrderStatusResponseDto&amp;gt; order(
            @Auth AuthUser owner,
            @PathVariable Long orderId,
            @RequestParam String status
    ) {
        return ResponseEntity.ok(ownerOrderService.order(owner.getId(), orderId, status));
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그러면 다음과 같이 호출할 수 있다. 주문 상태에 따라 status 값만 바꿔주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741336531202&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PATCH /api/v1/orders/1?status=ACCEPTED&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;32&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRo1hM/btsME8J9YRH/K2U0sIqZ1PIyDrEWjq5Bk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRo1hM/btsME8J9YRH/K2U0sIqZ1PIyDrEWjq5Bk0/img.png&quot; data-alt=&quot;이제 이거 하나만 있으면 된다..!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRo1hM/btsME8J9YRH/K2U0sIqZ1PIyDrEWjq5Bk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRo1hM%2FbtsME8J9YRH%2FK2U0sIqZ1PIyDrEWjq5Bk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;254&quot; height=&quot;32&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;32&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이제 이거 하나만 있으면 된다..!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그 다음 &lt;b&gt;OwnerOrderService&lt;/b&gt;로 가보면 이전과 달리 아주 심플해진 코드를 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;✔️ 사용자를 검증하는 코드를 private 메서드로 분리해 order() 메서드 내에서는 &lt;b&gt;validateOwner()&lt;/b&gt; 한 줄로 적었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;✔️ OrderStatus enum 클래스 내부에 추상 메서드 &lt;b&gt;update()&lt;/b&gt;를 선언해 역시 &lt;b&gt;OrderStatus.valueOf(status).update(order);&lt;/b&gt; 한 줄로 적었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741335959533&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 OwnerOrderService {

    private final UserOrderService userOrderService;

    @Transactional
    public OrderStatusResponseDto order(Long ownerId, Long orderId, String status) {
        UserOrderEntity order = userOrderService.getOrderById(orderId);
        validateOwner(ownerId, order);
        OrderStatus.valueOf(status).update(order);

        return OrderStatusResponseDto.from(order);
    }

    private void validateOwner(Long ownerId, UserOrderEntity order) {
        if (!Objects.equals(ownerId, order.getStore().getUser().getId())) {
            throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그러면 이제 변경사항이 생겨도 사실상 Controller와 Service 쪽은 건드릴 필요가 없다. OrderStatus 클래스에 각 상태에 따른 예외 처리를 넣어놨기 때문이다. 중복이 되는 부분은 따로 빼서 메서드로 만들고, order를 받아 조건이 맞으면 해당 order의 상태를 수정하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741335975703&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
public enum OrderStatus {

    PENDING(&quot;주문 확인전&quot;) {
        @Override
        public void update(UserOrderEntity order) {

        }
    },
    CANCELED(&quot;주문 취소&quot;) {
        @Override
        public void update(UserOrderEntity order) {
            validateOrderCancelable(order);
        }
    },
    ACCEPTED(&quot;주문 접수&quot;) {
        @Override
        public void update(UserOrderEntity order) {
            validateOrderCancelable(order);
            order.updateOrderStatus(OrderStatus.ACCEPTED);
        }
    },
    REJECTED(&quot;주문 거절&quot;) {
        @Override
        public void update(UserOrderEntity order) {
            validateOrderCancelable(order);
            order.updateOrderStatus(OrderStatus.REJECTED);
        }
    },
    COOKING(&quot;조리중&quot;) {
        @Override
        public void update(UserOrderEntity order) {
            validateNotDeliveringOrCompleted(order);
            if (!OrderStatus.ACCEPTED.equals(order.getOrderStatus())) {
                throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_ACCEPTED_BEFORE_COOKING.getMessage());
            }
            order.updateOrderStatus(OrderStatus.COOKING);
        }
    },
    DELIVERING(&quot;배달중&quot;) {
        @Override
        public void update(UserOrderEntity order) {
            validateNotDeliveringOrCompleted(order);
            if (!OrderStatus.COOKING.equals(order.getOrderStatus())) {
                throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_COOKING_BEFORE_DELIVERY.getMessage());
            }
            order.updateOrderStatus(OrderStatus.DELIVERING);
        }
    },
    COMPLETED(&quot;배달 완료&quot;) {
        @Override
        public void update(UserOrderEntity order) {
            if (!OrderStatus.DELIVERING.equals(order.getOrderStatus())) {
                throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_DELIVERING_BEFORE_COMPLETE.getMessage());
            }
            order.updateOrderStatus(OrderStatus.COMPLETED);
        }
    };

    private final String description;

    abstract public void update(UserOrderEntity order);

    private static void validateOrderCancelable(UserOrderEntity order) {
        if (!OrderStatus.PENDING.equals(order.getOrderStatus())) {
            throw new InvalidRequestException(ErrorOrderMessage.CANNOT_CANCEL_ORDER.getMessage());
        }
    }

    private static void validateNotDeliveringOrCompleted(UserOrderEntity order) {
        if (OrderStatus.DELIVERING.equals(order.getOrderStatus())
                || OrderStatus.COMPLETED.equals(order.getOrderStatus())) {
            throw new InvalidRequestException(ErrorOrderMessage.ALREADY_DELIVERING_OR_COMPLETED.getMessage());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 리팩토링을 진행한 뒤, Controller는 API 5개에서 1개로, Service의 중복 로직도 enum 내부로 분리되어 코드가 훨씬 간결해졌다. 이 과정을 통해 중복 코드에 대한 경각심을 다시 한 번 일깨웠고, enum 클래스에서 abstract 메서드를 사용하는 방법에 대해 학습했다. &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;</description>
      <category>TIL</category>
      <category>Abstract</category>
      <category>Enum</category>
      <category>Refactoring</category>
      <category>Spring</category>
      <category>Til</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/374</guid>
      <comments>https://3uomlkh.tistory.com/374#entry374comment</comments>
      <pubDate>Fri, 7 Mar 2025 21:22:48 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] JWT 시크릿 키 설정 및 문제 해결 / -hex 64 vs -base64 32 차이</title>
      <link>https://3uomlkh.tistory.com/373</link>
      <description>&lt;h2 id=&quot;main-실행-시-에러-발생&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Main 실행 시 에러 발생&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1280&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsTeAQ/btsMxl5ei1A/lvw4mgvJKmMYwdLZVWk4y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsTeAQ/btsMxl5ei1A/lvw4mgvJKmMYwdLZVWk4y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsTeAQ/btsMxl5ei1A/lvw4mgvJKmMYwdLZVWk4y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsTeAQ%2FbtsMxl5ei1A%2Flvw4mgvJKmMYwdLZVWk4y1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;675&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;에러 메세지가 길긴 하지만 잘 읽어보면&amp;nbsp;&lt;b&gt;JWT 시크릿 키&lt;/b&gt;에 대한 문제인 것을 짐작할 수 있다.&amp;nbsp;application.properties&amp;nbsp;파일에 키를 넣으려고 했는데, 아예&amp;nbsp;resourse&amp;nbsp;폴더가 없었다. 따라서 먼저 키를 만들어야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;java.lang.IllegalArgumentException: Could not resolve placeholder 'jwt.secret.key' in value &quot;${jwt.secret.key}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;Unsatisfied dependency expressed through constructor parameter 0: 
Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;터미널에 아래와 같이 입력해 키를 만들었다. 키를 처음 만들어 보지만 검색하니 만드는 방법이 바로 나왔다. 터미널로 만드는 방법 외에도 키를 만드는 사이트를 활용하는 방법도 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;openssl rand -hex 64&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;결과는 다음과 같이 아주 긴 시크릿 키가 나왔다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;22b8c9ae9a1b397ef0c8a2d142f91d1d00b7c0dc0dae01d2999d197f111f3e01c15a4477f63732fa2aa6fe8fbf313874b681fa31cef4049aa50707b7f8156&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그리고 &lt;b&gt;resourse&lt;/b&gt; 폴더를 생성 후,&amp;nbsp;&lt;b&gt;application.properties&amp;nbsp;&lt;/b&gt;파일을 만들고 다음과 같이 키를 넣었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;jwt.secret.key=&quot;JWT_시크릿_키&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여기서 중요한 점은 JwtUtil 클래스에서 &lt;b&gt;${jwt.secret.key}&lt;/b&gt; 로 적었다면, &lt;b&gt;jwt.secret.key&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;974&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PgyaV/btsMzRaj3yl/qknK3VXc0hzRaEFe8pLIU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PgyaV/btsMzRaj3yl/qknK3VXc0hzRaEFe8pLIU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PgyaV/btsMzRaj3yl/qknK3VXc0hzRaEFe8pLIU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPgyaV%2FbtsMzRaj3yl%2FqknK3VXc0hzRaEFe8pLIU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;974&quot; height=&quot;148&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DisCi/btsMzgBlynj/AtrJd5ezm3kiy5RujFjtV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DisCi/btsMzgBlynj/AtrJd5ezm3kiy5RujFjtV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DisCi/btsMzgBlynj/AtrJd5ezm3kiy5RujFjtV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDisCi%2FbtsMzgBlynj%2FAtrJd5ezm3kiy5RujFjtV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;766&quot; height=&quot;97&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;97&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그런데 또 에러가 났다. 왜일까? 잘 살펴보니 새로운 에러가 등장했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;java.lang.IllegalArgumentException: Illegal base64 character 22&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해당 에러에 대한 내용을 검색하다가 JWT를 사용한 다른 사람의 스크린샷을 보고 나의 실수를 알아차렸다. 바로&amp;nbsp;&lt;b&gt;쌍따옴표로 키를 감싼 것&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 작성하지 말고,&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;jwt.secret.key=&quot;JWT_시크릿_키&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 쌍따옴표 없이 작성해야 한다. 제대로 고친 후 실행하니 정상적으로 작동했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;jwt.secret.key=JWT_시크릿_키&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;+) 추가 공부  &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;처음에는 시크릿 키 값을 바꾼 것 때문에 정상 작동하는 줄 알았지만 아니었다. 키 값이 문제는 아니었지만, 내가 위에서 만든 키 값은 현재 프로젝트에서 사용하고 있는 알고리즘에 적합하지 않다는 것을 알게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;현재 프로젝트에서 적용한 알고리즘은&amp;nbsp;HS256&amp;nbsp;알고리즘이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;사용하는 알고리즘에 따라 키를 다르게 생성해야 한다. 내가 위에서 키를 생성하기 위해 사용한&amp;nbsp;openssl rand -hex 64&amp;nbsp;는 HS512(512비트) 알고리즘을 사용할 때 적합하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 다음과 같이 조금 다른 명령어를 gitbash에 치니 더 짧은 키가 나왔다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;openssl rand -base64 32&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;결과를 보니 확실히 위에서 얻은 키와 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;7yD9Da2chXAX5qCI2WrIzdiIifDZI8WPtcqJ+rjUJRA=&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;b&gt;-hex 64&amp;nbsp;vs&amp;nbsp;-base64 32&amp;nbsp;차이&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1)&amp;nbsp;&lt;b&gt;-hex 64&amp;nbsp;(HEX)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;HS512(512비트) 알고리즘을 사용할 때 적합&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다만, HEX 문자열은 Base64와 다르게 바로 Secret Key로 사용하려면 바이트 변환이 필요함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;예제 변환 코드&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;    byte[] keyBytes = new BigInteger(secretKey, 16).toByteArray();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2)&amp;nbsp;&lt;b&gt;-base64 32&amp;nbsp;(Base64)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;HS256(256비트) 알고리즘을 사용할 때 적합&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Base64.getDecoder().decode(secretKey)를 통해 바로 바이트 배열로 변환 가능&lt;/span&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7BOfK/btsMzQCtrXv/etAff9a5PFkMAk0X2pVJM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7BOfK/btsMzQCtrXv/etAff9a5PFkMAk0X2pVJM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7BOfK/btsMzQCtrXv/etAff9a5PFkMAk0X2pVJM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7BOfK%2FbtsMzQCtrXv%2FetAff9a5PFkMAk0X2pVJM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;676&quot; height=&quot;178&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey))와 같은 방식으로 사용 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그런데,&amp;nbsp;openssl rand -hex 64&amp;nbsp;를 이용해 생성한 키도 정상적으로 돌아가서 의문이 들었다. HS256 알고리즘에 적합한 키는&amp;nbsp;openssl rand -base64 32&amp;nbsp;로 생성한 키라고 했는데, 왜 정상적으로 작동하는 걸까? 이 부분은 GPT에게 물어봤다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그 이유는&amp;nbsp;&lt;b&gt;Spring Security 및 JJWT 라이브러리에서 내부적으로 HEX 문자열을 바이트 배열로 변환하여 처리하기 때문&lt;/b&gt;이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JJWT (Java JWT, JSON Web Token for Java) : Java 기반의 JWT(Json Web Token) 생성 및 검증을 위한 라이브러리로, JWT를 쉽게 다룰 수 있도록 도와준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;JJWT에서&amp;nbsp;Keys.hmacShaKeyFor()&amp;nbsp;메서드는 다음과 같이 동작한다.&amp;nbsp;byte[] keyBytes를 입력 받아&amp;nbsp;SecretKey&amp;nbsp;객체로 변환하는데, 여기서 입력값이 256비트보다 길면 자동으로 적절한 길이로 잘라서 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;public static SecretKey hmacShaKeyFor(byte[] keyBytes) {
    return new SecretKeySpec(keyBytes, SignatureAlgorithm.HS256.getJcaName());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;따라서, 키가 길어도 문제가 발생하지 않는다.&lt;/b&gt;&amp;nbsp;하지만 불필요하게 긴 값을 사용하기보단&amp;nbsp;Base64&amp;nbsp;키를 사용해 변환 과정을 없애는 것이 더 효율적일 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/373</guid>
      <comments>https://3uomlkh.tistory.com/373#entry373comment</comments>
      <pubDate>Thu, 27 Feb 2025 19:23:10 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @EntityGraph로 N+1문제 해결하기</title>
      <link>https://3uomlkh.tistory.com/372</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음&amp;nbsp;getTodos()&amp;nbsp;메서드에서 발생하고 있는 N+1 문제를&amp;nbsp;@EntityGraph를 사용해 해결해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;public Page&amp;lt;TodoResponse&amp;gt; getTodos(int page, int size) {
    Pageable pageable = PageRequest.of(page - 1, size);

    Page&amp;lt;Todo&amp;gt; todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);

    return todos.map(todo -&amp;gt; new TodoResponse(
            todo.getId(),
            todo.getTitle(),
            todo.getContents(),
            todo.getWeather(),
            new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()),
            todo.getCreatedAt(),
            todo.getModifiedAt()
    ));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위 코드에서 할일을 모두 불러온 후, 그 아래 유저를 또 다시 불러오는 과정에서 N+1 문제가 발생할 가능성이 높아보인다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Todo&amp;nbsp;엔티티의 일부를 살펴보면&amp;nbsp;User와&amp;nbsp;@ManyToOne&amp;nbsp;다대일 연관관계를 맺고 있고, 지연 로딩(FetchType.LAZY)으로 설정되어 있다. 지연 로딩으로 설정하면&amp;nbsp;Todo를 조회할 때&amp;nbsp;User를 바로 가져오는 것이 아니라 프록시 객체로 있다가 실제 사용될 때 SELECT 쿼리가 실행되는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;user_id&quot;, nullable = false)
private User user;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;먼저&amp;nbsp;findAllByOrderByModifiedAtDesc()&amp;nbsp;메서드를 통해 다음과 같이 모든 todo를 불러오고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;Page&amp;lt;Todo&amp;gt; todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이때 다음과 같은 쿼리가 실행될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;SELECT * FROM todo ORDER BY modified_at DESC LIMIT ?, ?&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그리고 다음 코드에서&amp;nbsp;todo.getUser()를 하면&amp;nbsp;User에 대한 SELECT 추가 쿼리가 실행된다. 즉 todo가 100개라면 위에서 1개의 쿼리를 실행하고, 아래에서 100개의 쿼리가 또 발생하게 되는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;todos.map(todo -&amp;gt; new TodoResponse(
        todo.getId(),
        todo.getTitle(),
        todo.getContents(),
        todo.getWeather(),
        new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()), // N+1 문제 발생 가능
        todo.getCreatedAt(),
        todo.getModifiedAt()
));&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Repository에서는 이미 다음과 같이&amp;nbsp;fetch join을 사용해 N+1 문제를 해결했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Query(&quot;SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC&quot;)
Page&amp;lt;Todo&amp;gt; findAllByOrderByModifiedAtDesc(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여기서&amp;nbsp;@EntityGraph라는 어노테이션을 활용해 해결해보자.&amp;nbsp;&lt;b&gt;@EntityGraph(attributePaths = {&quot;user&quot;})&lt;/b&gt;와 같이 적어주면&amp;nbsp;User도 함께 조회하도록 설정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@EntityGraph(attributePaths = {&quot;user&quot;})
Page&amp;lt;Todo&amp;gt; findAllByOrderByModifiedAtDesc(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;+) N+1 문제 눈으로 확인하기 &lt;/b&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;오늘 팀원분께서 N+1 문제를 해결하지 않았을 때의 결과와 해결했을 때의 결과를 직접 보여주셨다. 그래서 나도 따라서 테스트 해보니 훨씬 이해가 잘됐다. 미리 유저 2명을 생성하고, 일정도 각 유저마다 1개씩 만들어 테스트 했더니 다음과 같은 결과가 나왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JSkVj/btsMxpkTZ0F/SDtzstsKFyjtKoK3xZ6T3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JSkVj/btsMxpkTZ0F/SDtzstsKFyjtKoK3xZ6T3k/img.png&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;1809&quot; data-is-animation=&quot;false&quot; style=&quot;width: 27.2117%; margin-right: 10px;&quot; data-widthpercent=&quot;27.53&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JSkVj/btsMxpkTZ0F/SDtzstsKFyjtKoK3xZ6T3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJSkVj%2FbtsMxpkTZ0F%2FSDtzstsKFyjtKoK3xZ6T3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;528&quot; height=&quot;1809&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckJ9Ve/btsMxDXDchp/10gvvxl60EMkCYL8wcRdxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckJ9Ve/btsMxDXDchp/10gvvxl60EMkCYL8wcRdxK/img.png&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;712&quot; data-is-animation=&quot;false&quot; style=&quot;width: 71.6255%;&quot; data-widthpercent=&quot;72.47&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckJ9Ve/btsMxDXDchp/10gvvxl60EMkCYL8wcRdxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckJ9Ve%2FbtsMxDXDchp%2F10gvvxl60EMkCYL8wcRdxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;547&quot; height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; @EntityGraph 적용 전(왼) / @EntityGraph 적용 후(오) &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;N+1 문제를 해결하기 전에는 일정을 조회하고, 각 유저마다 한 번씩 더 조회하고 있다. 유저의 수(2명)만큼 쿼리가 실행된 것이다. N+1 문제를 해결한 후에는 유저를 추가로 조회하는 것 없이 쿼리가 한 번만 실행되는 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>@EntityGraph</category>
      <category>n+1</category>
      <category>Spring</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/372</guid>
      <comments>https://3uomlkh.tistory.com/372#entry372comment</comments>
      <pubDate>Wed, 26 Feb 2025 00:14:37 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring에서 세션을 다루는 3가지 방법, 뭐가 제일 좋을까?</title>
      <link>https://3uomlkh.tistory.com/371</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;튜터님의 해설 강의를 듣고 일정 관리 앱 과제를 리뷰하다가 알게 된 내용을 간단하게 포스팅 해본다. 이번 일정 관리 앱 과제에서 튜터님의 코드와 비교했을때, 가장 큰 차이 중 하나는 &lt;b&gt;세션을 사용하는 방식&lt;/b&gt;이었다. 나는&amp;nbsp;&lt;b&gt;HttpSession&lt;/b&gt;을&amp;nbsp;&lt;b&gt;직접 가져와 사용하는 방식&lt;/b&gt;을 사용했다. 반면 튜터님은&amp;nbsp;&lt;b&gt;@SessionAttribute&lt;/b&gt;&amp;nbsp;어노테이션을 사용하여&amp;nbsp;&lt;b&gt;세션 속성을 자동 주입하는 방식&lt;/b&gt;을 사용하셨고, 그 방법이 더 쉽게 세션을 사용할 수 있는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;세션을-사용하는-방식&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;  세션을 사용하는 방식&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 id=&quot;httpsession-사용하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;HttpSession 사용하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;내가 작성한 댓글 생성 기능 부분이다.&amp;nbsp;HttpServletRequest에서&amp;nbsp;HttpSession을 가져와 세션키를 사용하고 있다. 이 방법은 Servlet API에서 제공하는 기본적인 세션 관리 방식으로, 직접&amp;nbsp;HttpSession&amp;nbsp;객체를 사용하여 값을 저장하고 가져온다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@PostMapping(&quot;/schedules/{scheduleId}/comments&quot;)
public ResponseEntity&amp;lt;CommentResponseDto&amp;gt; save(
    @PathVariable Long scheduleId,
    @Valid @RequestBody CommentRequestDto dto,
    HttpServletRequest request
) {
    HttpSession session = request.getSession(false);
    String sessionKey = (String) session.getAttribute(&quot;sessionKey&quot;);

    CommentResponseDto commentResponseDto = commentService.save(scheduleId, sessionKey, dto);
    return new ResponseEntity&amp;lt;&amp;gt;(commentResponseDto, HttpStatus.CREATED);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 경우 한 눈에 보기에도 코드가 길어 간결해보이지 않는다는 단점이 있고, 만약 세션이 없을 경우&amp;nbsp;NullpointerException이 발생할 가능성이 있다. 또한&amp;nbsp;session.getAttribute()를 호출할 때 형 변환이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실제로 코드를 짜면서 아래 코드가 굉장히 많이 반복되어서 찝찝했는데 시간이 없어서 그냥 제출했다. 리뷰를 하면서 해당 코드가 있는 부분을 고치는데 너무 많아서 힘들었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;HttpSession session = request.getSession(false);
String sessionKey = (String) session.getAttribute(&quot;sessionKey&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;sessionattribute-사용하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@SessionAttribute 사용하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위 코드는&amp;nbsp;&lt;b&gt;@SesstionAttribute&lt;/b&gt;&amp;nbsp;어노테이션을 사용해 더 간결하게 바꿀 수 있다.&amp;nbsp;@SessionAttribute는 &lt;b&gt;특정 값을 컨트롤러 메서드에서 세션 속성으로 직접 매핑&lt;/b&gt;한다. 자동으로 세션값을 주입해 주기 때문에 불필요한 코드가 사라진다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@PostMapping(&quot;/schedules/{scheduleId}/comments&quot;)
public ResponseEntity&amp;lt;CommentResponseDto&amp;gt; save(
    @SessionAttribute(name = &quot;LOGIN_USER&quot;) Long userId,
    @PathVariable Long scheduleId,
    @RequestBody CommentSaveRequestDto dto
) {
    return ResponseEntity.ok(commentService.save(userId, scheduleId, dto));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 경우에는 첫 번째 방법보다 코드도 짧고 유지보수하기 쉽다. 세션을 가져오고, 속성을 읽은 후 형 변환까지 해야하는 과정 없이 바로 세션값을 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;handlermethodargumentresolver-사용하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;HandlerMethodArgumentResolver 사용하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;세션을 사용하는 방법이 더 있는지 찾아보다가 발견한 방식이다.&amp;nbsp;&lt;b&gt;HandlerMethodArgumentResolver&lt;/b&gt;를 구현하여, 특정 세션 값을 자동으로 컨트롤러 매개변수로 주입하는 방식이다. 컨트롤러에서 매번&amp;nbsp;@SessionAttribute나&amp;nbsp;HttpSession을 호출할 필요 없이, 자동으로 특정 객체를 주입받을 수 있도록 설정할 수 있다.&amp;nbsp;&lt;b&gt;세션을 가져오는 로직을 전역적으로 한 곳에서 관리&lt;/b&gt;할 수 있기 때문에 더 깔끔하고 유지보수가 쉽다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용하는 방법은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) @LoginUser 어노테이션 생성&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;먼저 LoginUser라는 커스텀 어노테이션을 만든다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@Target(ElementType.PARAMETER)&amp;nbsp;: 메서드의 파라미터에만 사용할 수 있도록 제한&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@Retention(RetentionPolicy.RUNTIME)&amp;nbsp;: 런타임에도 어노테이션이 유지되어 ArgumentResolver에서 인식할 수 있도록 설정&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RetentionPolicy.SOURCE: 소스 코드에서만 유지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RetentionPolicy.CLASS: 클래스 파일에만 유지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RetentionPolicy.RUNTIME: 런타임 시에도 유지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) HandlerMethodArgumentResolver 구현&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@LoginUser를 사용하면 자동으로 세션에서 값을 가져오도록&amp;nbsp;HandlerMethodArgumentResolver를 구현한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // @LoginUser 애노테이션이 있고, Long 타입일 때만 적용
        return parameter.getParameterAnnotation(LoginUser.class) != null
                &amp;amp;&amp;amp; parameter.getParameterType().equals(Long.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        // 요청 객체 및 세션 가져오기
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);

        if (session == null) {
            throw new RuntimeException(&quot;세션이 존재하지 않습니다.&quot;);
        }

        return userId;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3) WebConfig에 등록&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;addArgumentResolvers()에서 위에서 만든&amp;nbsp;LoginUserArgumentResolver를 SPring의 ArgumentResolver 목록에 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final LoginUserArgumentResolver loginUserArgumentResolver;

    public WebConfig(LoginUserArgumentResolver loginUserArgumentResolver) {
        this.loginUserArgumentResolver = loginUserArgumentResolver;
    }

    @Override
    public void addArgumentResolvers(List&amp;lt;HandlerMethodArgumentResolver&amp;gt; resolvers) {
        resolvers.add(loginUserArgumentResolver);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 하면 컨트롤러에서&amp;nbsp;@LoginUser&amp;nbsp;어노테이션을 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4) 컨트롤러에서 @LoginUser 사용&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이제 다음과 같이&amp;nbsp;@LoginUser만 붙이면 세션값을 가져올 수 있다. 아마 제일 간결한 방식이지 않을까 싶다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@PostMapping(&quot;/schedules/{scheduleId}/comments&quot;)
public ResponseEntity&amp;lt;CommentResponseDto&amp;gt; save(
        @LoginUser Long userId,  // 세션에서 자동으로 userId를 주입
        @PathVariable Long scheduleId,
        @RequestBody CommentSaveRequestDto dto
) {
    return ResponseEntity.ok(commentService.save(userId, scheduleId, dto));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그 외...&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Spring Security&lt;/b&gt;&amp;nbsp;: 세션 대신 SecurityContext에 사용자 정보 저장 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Spring Session&lt;/b&gt;&amp;nbsp;: 서버 메모리가 아닌 Redis, JDBC, Hazelcast 같은 저장소에 세션 저장 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실무에서는 Spring Session + Redis를 많이 사용한다고 한다.(GPT가 한 말이라 100% 신뢰할 순 없지만...)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실무에서 세션을 사용하는 방식은 나중에 더 많은 것을 배우고 나서 다시 공부해보면 좋을 것 같다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/371</guid>
      <comments>https://3uomlkh.tistory.com/371#entry371comment</comments>
      <pubDate>Sat, 22 Feb 2025 14:00:58 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Converter와 Fomatter 사용하기</title>
      <link>https://3uomlkh.tistory.com/370</link>
      <description>&lt;h3 id=&quot;converter-사용하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Converter 사용하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Spring은 기본적인 컨버터를 제공하지만, 컨버터를 커스텀하게 구현할 수도 있다. 예를 들면, String 값을 특정 객체를 변환하고 싶다면 커스텀 컨버터를 구현해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;강의 실습을 따라&amp;nbsp;String&amp;nbsp;값을&amp;nbsp;Person&amp;nbsp;객체로 변환하는 컨버터를 구현해보자.&amp;nbsp;Person&amp;nbsp;객체는&amp;nbsp;name과&amp;nbsp;age&amp;nbsp;필드를 갖는다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Controller에서 URL 파라미터를&amp;nbsp;Person&amp;nbsp;객체로 반환해야 한다면 커스텀 컨버터를 구현 후,&amp;nbsp;WebMvcConfigurer&amp;nbsp;에서 등록해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@GetMapping(&quot;/type-converter&quot;)
public void typeConverter(@RequestParam Person person) {
    log.info(&quot;person.getName() = {}&quot;, person.getName());
    log.info(&quot;person.getAge() = {}&quot;, person.getAge());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이&amp;nbsp;Converter&amp;nbsp;인터페이스를&amp;nbsp;implements&amp;nbsp;한&amp;nbsp;StringToPersonConverter&amp;nbsp;클래스를 만든다. 그리고&amp;nbsp;convert()&amp;nbsp;메서드를 오버라이드 한다.&amp;nbsp;convert()&amp;nbsp;메서드 내부에서는&amp;nbsp;&quot;gom:1200&quot;&amp;nbsp;과 같은 문자열이 들어오면&amp;nbsp;:&amp;nbsp;를 기준으로 나눈 후 이름과 나이를 구해 새로운&amp;nbsp;Person&amp;nbsp;객체를 반환하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public class StringToPersonConverter implements Converter&amp;lt;String, Person&amp;gt; {
    
    // source = &quot;gom:1200&quot;
    @Override
    public Person convert(String source) {
    
        String[] parts = source.split(&quot;:&quot;);
        String name = parts[0];
        
        int months = Integer.parseInt(parts[1]);
        int age = months / 12;
        
        return new Person(name, age);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그 다음&amp;nbsp;WebMvcConfigurer&amp;nbsp;인터페이스를&amp;nbsp;implements&amp;nbsp;한&amp;nbsp;WebConfig&amp;nbsp;클래스를 만든다.&amp;nbsp;WebConfig&amp;nbsp;클래스에 다음과 같이 커스텀 컨버터를 등록해야 한다. 오버라이드 한&amp;nbsp;addFormatters()&amp;nbsp;메서드 내부에&amp;nbsp;registry.addConverter()를 통해 등록하자.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToPersonConverter());
        registry.addConverter(new PersonToStringConverter());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이제 실행 후 Postman에서 테스트해본다. 정상적으로 작동하는 것을 볼 수 있다.&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;1056&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IeG7I/btsMsM86Yu4/ho5G94Q35dKVXL3Rgns4X1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IeG7I/btsMsM86Yu4/ho5G94Q35dKVXL3Rgns4X1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IeG7I/btsMsM86Yu4/ho5G94Q35dKVXL3Rgns4X1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIeG7I%2FbtsMsM86Yu4%2Fho5G94Q35dKVXL3Rgns4X1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1056&quot; height=&quot;804&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1280&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnqlao/btsMsLh01QG/SsdLS1vpI21nrsAtyAaao0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnqlao/btsMsLh01QG/SsdLS1vpI21nrsAtyAaao0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnqlao/btsMsLh01QG/SsdLS1vpI21nrsAtyAaao0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcnqlao%2FbtsMsLh01QG%2FSsdLS1vpI21nrsAtyAaao0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;83&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;fomatter-사용하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Fomatter 사용하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Fomatter는&amp;nbsp;Converter&amp;nbsp;보다 더 세밀한 문자열 포맷팅을 할 수 있는 기능이다.&amp;nbsp;Formatter&amp;nbsp;인터페이스를&amp;nbsp;implements&amp;nbsp;해서 날짜, 숫자 같은 특정 데이터 포맷으로 변환해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이&amp;nbsp;Formatter&amp;nbsp;인터페이스를&amp;nbsp;implements한&amp;nbsp;PriceFormatter&amp;nbsp;클래스를 만든다. 그리고&amp;nbsp;parse()&amp;nbsp;메서드와&amp;nbsp;print()&amp;nbsp;메서드를 오버라이드한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Slf4j
public class PriceFormatter implements Formatter&amp;lt;Number&amp;gt; {

    @Override
    public Number parse(String text, Locale locale) throws ParseException {
        log.info(&quot;text = {}, locale={}&quot;, text, locale);

        // 기본 제공되는 기능 숫자 중간에 (,) 쉼표를 넣어준다.
        NumberFormat format = NumberFormat.getInstance(locale);
        return format.parse(text);
    }

    @Override
    public String print(Number object, Locale locale) {
        log.info(&quot;object = {}, locale = {}&quot;, object, locale);
        return NumberFormat.getInstance(locale).format(object);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Locale : 지역 및 언어 정보를 나타내는 객체. Locale 정보를 활용하여 나라별로 다른 숫자 포맷으로 만들어준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;컨버터와 같이&amp;nbsp;WebConfig에 등록해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new PriceFormatter());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Postman에서 잘 실행되는 모습을 볼 수 있다.&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;1056&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y3RwN/btsMr8xIDNq/kzqS67O87OMJZ2YVAZiKO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y3RwN/btsMr8xIDNq/kzqS67O87OMJZ2YVAZiKO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y3RwN/btsMr8xIDNq/kzqS67O87OMJZ2YVAZiKO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy3RwN%2FbtsMr8xIDNq%2FkzqS67O87OMJZ2YVAZiKO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1056&quot; height=&quot;737&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;인텔리제이에서도 잘....되나? 했는데 당연히 위에 적어놓은 Log가 찍힐 줄 알았는데 찍히지 않았다. 찾아보니 Spring이 내부적으로&amp;nbsp;StringToBigDecimalConverter과 같은 기본 변환기를 사용해 변환할 수 있기 때문에&amp;nbsp;PriceFormatter가 실행되지 않을 가능성이 있다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그래서&amp;nbsp;addFormatterForFieldType()을 사용해 특정 타입에 대해 강제로&amp;nbsp;PriceFormatter를 사용하도록 설정하니 로그가 제대로 찍혔다. 하지만 기본 변환기로 변환이 되는거라면 굳이 강제로 사용하게 할 이유는 없는 것 같다..  이 부분은 더 알아봐야겠다.&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;1280&quot; data-origin-height=&quot;48&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFBGtY/btsMrDdLNU7/ygfyFXS6sbNyVW0tkc61eK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFBGtY/btsMrDdLNU7/ygfyFXS6sbNyVW0tkc61eK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFBGtY/btsMrDdLNU7/ygfyFXS6sbNyVW0tkc61eK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFBGtY%2FbtsMrDdLNU7%2FygfyFXS6sbNyVW0tkc61eK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;48&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;48&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>TIL</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/370</guid>
      <comments>https://3uomlkh.tistory.com/370#entry370comment</comments>
      <pubDate>Fri, 21 Feb 2025 23:58:48 +0900</pubDate>
    </item>
    <item>
      <title>[Git] 로컬 브랜치에서 pull 할 때 에러 발생 - stash로 임시 커밋하기</title>
      <link>https://3uomlkh.tistory.com/369</link>
      <description>&lt;h1&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;문제점&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;develop 브랜치에서 &lt;b&gt;git pull origin develop&lt;/b&gt;을 하려는데 에러가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;하지만 현재 내 로컬 브랜치에서의 작업이 아직 끝나지 않아서 commit하고 싶지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해결 방법&lt;/span&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;변경 사항을 유지하면서 git pull 하는 방법으로 진행했다.&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;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1) 변경 사항 임시 저장&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739888790342&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git stash push -m &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;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2) 원격 저장소에서 최신 코드 가져오기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;git switch develop
git pull origin develop
&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;로컬 develop 브랜치에 원격 develop 코드를 가져올 것이기 때문에 develop 브랜치로 이동 후 진행했다.&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;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;3) 다시 내 브랜치로 이동 후 변경 사항 복구&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739888827773&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git checkout LCW git stash pop # 임시 저장한 변경 사항 복원&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 하면 커밋하지 않고 변경 사항을 저장했다가 다시 불러올 수 있다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>git</category>
      <category>stash</category>
      <category>Til</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/369</guid>
      <comments>https://3uomlkh.tistory.com/369#entry369comment</comments>
      <pubDate>Tue, 18 Feb 2025 23:28:08 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 왜 엔티티에 Setter를 사용하면 안 될까?</title>
      <link>https://3uomlkh.tistory.com/368</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 강의를 참고하여 비밀번호 수정 기능을 구현하던 중, 왜 단순히 Setter를 사용하지 않고 updatePassword() 메서드를 따로 만들어야 하는지&amp;nbsp;의문이 들었다. &lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;public void updatePassword(String password) {
    this.password = password;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Member 엔티티에서 아래와 같이&amp;nbsp;@Setter를 password 필드 위에 추가하면 안되는 것일까?&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Setter
@Column(nullable = false)
private String password;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;찾아보니 Setter의 사용을 지양하는 이유는 여러가지가 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2229&quot; data-start=&quot;2188&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1️⃣ &lt;b&gt;Setter를 사용하면 객체의 일관성을 유지하기 어렵다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2273&quot; data-start=&quot;2233&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2273&quot; data-start=&quot;2233&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;어디서든 값이 변경될 수 있어 데이터 무결성이 깨질 위험이 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2312&quot; data-start=&quot;2275&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2️⃣ &lt;b&gt;Setter를 사용하면 변경의 의도가 불분명하다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2383&quot; data-start=&quot;2316&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2383&quot; data-start=&quot;2316&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;setPassword()만 보면 이 메서드가 비밀번호 변경을 위한 것인지, 처음 설정하는 것인지 알기 어렵다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2418&quot; data-start=&quot;2385&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;3️⃣ &lt;b&gt;비즈니스 로직을 명확하게 분리할 수 없다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2467&quot; data-start=&quot;2422&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2467&quot; data-start=&quot;2422&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;비밀번호 변경 시 기존 비밀번호 검증 같은 추가 로직이 필요할 수도 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2495&quot; data-start=&quot;2469&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;4️⃣ &lt;b&gt;결과적으로 유지보수가 어렵다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2558&quot; data-start=&quot;2499&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2558&quot; data-start=&quot;2499&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여러 곳에서 Setter를 호출해버리면, 이후 로직 변경이 필요할 때 모든 코드에서 변경해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 결국, Setter를 사용하면 데이터의 무결성이 깨질 가능성이 높고, 유지보수도 어려워진다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서, 비밀번호 변경 로직을 updatePassword() 같은 명확한 메서드로 분리하는 것이 바람직하다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 하면 객체의 일관성을 유지하면서, 비즈니스 로직을 더욱 명확하게 관리할 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Til</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/368</guid>
      <comments>https://3uomlkh.tistory.com/368#entry368comment</comments>
      <pubDate>Thu, 13 Feb 2025 23:56:16 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] INSERT 됐지만 테이블이 안 보이는 문제 - @Transactional</title>
      <link>https://3uomlkh.tistory.com/367</link>
      <description>&lt;h3 id=&quot;insert-됐지만-테이블이-안보이는-문제&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;INSERT 됐지만 테이블이 안보이는 문제&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;댓글 작성 시, 아래와 같이 INSERT문은 제대로 실행된 것 같은데 DB에서는 comment 테이블이 보이지 않는 문제가 있었다.&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;1240&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMLSBS/btsMgMHyD3S/Haf11kXkPGjMiS0kw5nm51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMLSBS/btsMgMHyD3S/Haf11kXkPGjMiS0kw5nm51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMLSBS/btsMgMHyD3S/Haf11kXkPGjMiS0kw5nm51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMLSBS%2FbtsMgMHyD3S%2FHaf11kXkPGjMiS0kw5nm51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1240&quot; height=&quot;351&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;351&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;@Transactional을 추가하지 않은 것이 원인이다. 트랜잭션이 자동으로 커밋되지 않아서 데이터가 DB에 반영되지 않았던 것이다. Service 계층에서 DB 작업을 수행할 때는 반드시&amp;nbsp;@Transactional&amp;nbsp;을 사용해 트랜잭션을 관리해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이&amp;nbsp;@Transactional&amp;nbsp;을 붙인다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Transactional
public CommentResponseDto save(Long scheduleId, String email, CommentRequestDto dto) {
    Schedule schedule = scheduleRepository.findByIdOrElseThrow(scheduleId);
    Member member = memberRepository.findByEmailOrElseThrow(email);

    Comment comment = new Comment(dto.getContents(), member, schedule);
    Comment savedComment = commentRepository.save(comment);

    return new CommentResponseDto(
        savedComment.getId(),
        savedComment.getContents(),
        member.getUsername(),
        savedComment.getCreatedAt(),
        savedComment.getModifiedAt()
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 해도 되지만, 어제 &amp;lt;연관 관계 매핑 세션&amp;gt;에서 튜터님께서 클래스 레벨에 우선@Transactional(readOnly = true)를 사용한다는 것이 기억나 다음과 같이 수정했다. 그리고 지난번 피드백을 다시 보니 다른 튜터님께서도&amp;nbsp;&lt;b&gt;&quot;트랜잭션 범위를 확실히 하기 위해 DB 업데이트와 관련된 모든 작업을 트랜잭션 내에서 처리하는 것이 좋다.&quot;&lt;/b&gt;라고 피드백을 남겨주셨다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentService {

    private final CommentRepository commentRepository;
    private final ScheduleRepository scheduleRepository;
    private final MemberRepository memberRepository;

    @Transactional
    public CommentResponseDto save(Long scheduleId, String email, CommentRequestDto dto) {
        Schedule schedule = scheduleRepository.findByIdOrElseThrow(scheduleId);
        Member member = memberRepository.findByEmailOrElseThrow(email);

        Comment comment = new Comment(dto.getContents(), member, schedule);
        Comment savedComment = commentRepository.save(comment);

        return new CommentResponseDto(
            savedComment.getId(),
            savedComment.getContents(),
            member.getUsername(),
            savedComment.getCreatedAt(),
            savedComment.getModifiedAt()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이 문제를 해결하면서 잘 신경쓰지 않았던 부분의 개념을 확실히 익히게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;클래스 레벨의&amp;nbsp;@Transactional(readOnly = true)&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기본적으로 모든 메서드는 읽기 전용 트랜잭션 사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;변경 감지(Dirty Checking) 비활성화 &amp;rarr; 불필요한 데이터 변경 감지 로직 생략 &amp;rarr; 성능 최적화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;조회 시&amp;nbsp;@Transactional이 없어도 동작하지만,&amp;nbsp;@Transactional(readOnly = true)를 사용하면 성능이 최적화 되며 Lazy Loading(지연 로딩) 사용 시 트랜잭션 유지가 필요할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;쓰기 작업에는&amp;nbsp;@Transactional을 개별적으로 추가해야 함.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;메서드 레벨에서&amp;nbsp;@Transactional을 명시하면, 클래스 레벨의&amp;nbsp;readOnly = true를 무시하고 데이터 변경이 가능한 트랜잭션으로 동작&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>Til</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/367</guid>
      <comments>https://3uomlkh.tistory.com/367#entry367comment</comments>
      <pubDate>Wed, 12 Feb 2025 23:30:18 +0900</pubDate>
    </item>
    <item>
      <title>[Java/Study] 김영한의 실전 자바 중급 2편 - 스터디 15회차</title>
      <link>https://3uomlkh.tistory.com/366</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;i&gt;인프런 강의 &amp;lt;김영한의 실전 자바 - 중급 2편&amp;gt;을 보고 정리한 내용입니다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;i&gt;매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; 컬렉션 프레임워크 - Map,&amp;nbsp;Stack,&amp;nbsp;Queue&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Map&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt;Map은 키-값의 쌍을 저장하는 자료 구조로, HashMap이 많이 사용된다!&lt;/i&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt;키 중복 불가능 / 값은 중복 가능 / 순서 유지X&amp;nbsp;&lt;/i&gt;&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;자바는 Map 인터페이스의 메서드를 구현하는 HashMap, TreeMap, LinkedHashMap 등 다양한 Map 구현체를 제공한다.&amp;nbsp; Map에서 키만 따로 떼어놓고 보면 Set과 똑같기 때문에 Set과 Map의 구현체는 거의 같다. 실제로 HashSet은 내부적으로 HashMap을 사용하여 구현된다. 따라서 각각의 특징도 이전에 배웠던 내용과 거의 흡사해 간략하게만 정리했다.&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_KakaoTalk_20250207_003510073.jpg&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;952&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTIhpX/btsL9nu0vks/7IfRkXegfKmp9MkhV7IuX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTIhpX/btsL9nu0vks/7IfRkXegfKmp9MkhV7IuX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTIhpX/btsL9nu0vks/7IfRkXegfKmp9MkhV7IuX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTIhpX%2FbtsL9nu0vks%2F7IfRkXegfKmp9MkhV7IuX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1115&quot; height=&quot;952&quot; data-filename=&quot;edited_KakaoTalk_20250207_003510073.jpg&quot; data-origin-width=&quot;1115&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;letter-spacing: 0px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음은 String과 Integer 타입을 키-값으로 사용하는 map을 각각의 구현체로 돌린 결과다. &lt;span style=&quot;text-align: start;&quot;&gt;간단한 코드라서 자바 코드를 모두 가져오진 않았지만, 결과를 보면 각 구현체의 특징을 알 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738858088914&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// map에 넣은 데이터 순서
map.put(&quot;C&quot;, 10);
map.put(&quot;B&quot;, 20);
map.put(&quot;A&quot;, 30);
map.put(&quot;1&quot;, 40);
map.put(&quot;2&quot;, 50);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1738857846716&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;map = class java.util.HashMap
A=30 1=40 B=20 2=50 C=10

map = class java.util.LinkedHashMap
C=10 B=20 A=30 1=40 2=50

map = class java.util.TreeMap
1=40 2=50 A=30 B=20 C=10&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;u&gt;&lt;/u&gt;✔️ Map 인터페이스 주요 메서드 정리 &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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;put(K key, V value) : 지정된 키와 값을 맵에 저장한다. (같은 키가 있으면 값 변경)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;putAll(Map&amp;lt;? extends K, ? extends V&amp;gt; m) : 지정된 맵의 모든 매핑을 현재 맵에 복사한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;putIfAbsent(K key, V value) : 지정된 키가 없는 경우에 키와 값을 맵에 저장한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;get(Object key) : 지정된 키에 연결된 값을 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;getOrDefault(Object key, V defaultValue) : 지정된 키에 연결된 값을 반환한다. 키가 없는 경우 defaultValue로 지정한 값을 대신 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;remove(Object key) : 지정된 키와 그에 연결된 값을 맵에서 제거한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;clear() : 맵에서 모든 키와 값을 제거한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;containsKey(Object key) : 맵이 지정된 키를 포함하고 있는지 여부를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;containsValue(Object value) : 맵이 하나 이상의 키에 지정된 값을 연결하고 있는지 여부를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;keySet() : 맵의 키들을 Set 형태로 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;values() : 맵의 값들을 Collection 형태로 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;entrySet() : 맵의 키-값 쌍을 Set&amp;lt;Map.Entry&amp;lt;K, V&amp;gt;&amp;gt; 형태로 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;size() : 맵에 있는 키-값 쌍의 개수를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;isEmpty() : 맵이 비어 있는지 여부를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ Map 순회하기&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이 studentMap에 학생의 이름(키)과 점수(값)를 넣었다고 한다면, 어떻게 키와 값을 조회할 수 있을까?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738856488340&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Map&amp;lt;String, Integer&amp;gt; studentMap = new HashMap&amp;lt;&amp;gt;();

studentMap.put(&quot;studentA&quot;, 90);
studentMap.put(&quot;studentB&quot;, 80);
studentMap.put(&quot;studentC&quot;, 80);
studentMap.put(&quot;studentD&quot;, 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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1) 키 목록 조회: keySet()&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738856681495&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Set&amp;lt;String&amp;gt; keySet = studentMap.keySet();
for (String key : keySet) {
    Integer value = studentMap.get(key);
    System.out.println(&quot;key = &quot; + key + &quot;, value = &quot; + value);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;map.keySet()를 활용해 키의 목록을 조회할 수 있다. Map에서 키만 따로 떼어놓고 보면 Set과 똑같기 때문에 keySet()을 호출하면 Set을 반환한다. 여기서 얻은 키를 활용해 map.get()를 이용하면 해당 키에 따른 값도 구할 수 있다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;2) 값 목록 조회: values()&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738856694768&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Collection&amp;lt;Integer&amp;gt; values = studentMap.values();
for (Integer value : values) {
    System.out.println(&quot;value = &quot; + 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;map.values()를 활용해 값의 목록을 조회할 수 있다. 여기서 값은 중복을 허용하고, 입력 순서를 보장하지 않기 때문에 Set이나 List로 반환하기 애매하다. 따라서 상위 인터페이스인 Collection으로 반환한다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;3) 키와 값 목록 조회: entrySet()&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738856712710&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Set&amp;lt;Map.Entry&amp;lt;String, Integer&amp;gt;&amp;gt; entries = studentMap.entrySet();
for (Map.Entry&amp;lt;String, Integer&amp;gt; entry : entries) {
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println(&quot;key = &quot; + key + &quot;, value = &quot; + 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;❔Entry란 Map 내부에 있는 인터페이스로, Map 내부에서 키와 값을 하나로 묶어 저장할 때 사용하는 객체다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;951&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wKlXE/btsL8NOP9LS/ABeDBZvpckwTS7asLMWoP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wKlXE/btsL8NOP9LS/ABeDBZvpckwTS7asLMWoP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wKlXE/btsL8NOP9LS/ABeDBZvpckwTS7asLMWoP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwKlXE%2FbtsL8NOP9LS%2FABeDBZvpckwTS7asLMWoP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;379&quot; data-origin-width=&quot;951&quot; data-origin-height=&quot;600&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 map.entrySet()를 활용해 Entry 객체들을 가져와 각 Entry를 하나씩 꺼내면서 키와 값을 조회할 수 있다. 키를 가져올 때는 각 Entry에서 getKey(), 값을 가져올 때는 getValue()를 사용한다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; HashMap 은 HashSet 과 작동 원리가 같다. Set과 비교했을때 차이점은,&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;키를 사용해 해시 코드 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Entry를 사용해 &lt;span style=&quot;text-align: start;&quot;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Map은 키를 저장하는 구조에서 해시 테이블을 활용한다.&amp;nbsp; 따라서 Map의 키로 사용되는 객체는 hashCode() , equals() 를 반드시 구현해야 한다!&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Stack&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;i&gt;&lt;u&gt;Stack은 나중에 넣은 것이 가장 먼저 나오는 후입 선출(LIFO, Last In First Out) 자료구조다!&lt;/u&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;i&gt;&lt;u&gt;단, Stack 클래스 대신 Deque를 사용하자!&lt;/u&gt;&lt;/i&gt;&lt;i&gt;&lt;u&gt;&lt;/u&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;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;push(E item) : 스택의 맨 위에 새로운 요소를 추가한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;pop() : 스택의 맨 위에 있는 요소를 제거하고 반환한다. (스택이 비어있으면 예외 발생)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;peek() : 스택의 맨 위 요소를 제거하지 않고 반환한다. (스택이 비어있으면 예외 발생)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;isEmpty() : 스택이 비어있는지 확인한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;size() : 스택에 있는 요소의 개수를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-07 011836.png&quot; data-origin-width=&quot;1287&quot; data-origin-height=&quot;523&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/61xdT/btsMaN696xP/lfPlXVlJOotPf4MXlDxveK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/61xdT/btsMaN696xP/lfPlXVlJOotPf4MXlDxveK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/61xdT/btsMaN696xP/lfPlXVlJOotPf4MXlDxveK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F61xdT%2FbtsMaN696xP%2FlfPlXVlJOotPf4MXlDxveK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1287&quot; height=&quot;523&quot; data-filename=&quot;스크린샷 2025-02-07 011836.png&quot; data-origin-width=&quot;1287&quot; data-origin-height=&quot;523&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-07 011846.png&quot; data-origin-width=&quot;1279&quot; data-origin-height=&quot;535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsdBgB/btsL8Mh4i8P/eVI3Lq9I0RCwhzXD84eYN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsdBgB/btsL8Mh4i8P/eVI3Lq9I0RCwhzXD84eYN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsdBgB/btsL8Mh4i8P/eVI3Lq9I0RCwhzXD84eYN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsdBgB%2FbtsL8Mh4i8P%2FeVI3Lq9I0RCwhzXD84eYN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1279&quot; height=&quot;535&quot; data-filename=&quot;스크린샷 2025-02-07 011846.png&quot; data-origin-width=&quot;1279&quot; data-origin-height=&quot;535&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;자바의 Stack 클래스는 내부에서 지금은 사용되지 않는 Vector라는 자료구조를 사용한다. 따라서 Stack 대신 더 빠르고 좋은 Deque를 사용할 것을 권장한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Queue&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt;Queue는 가장 먼저 넣은 것이 가장 먼저 나오는 선입 선출(FIFO, First In First Out) 자료구조다!&lt;/i&gt;&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;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Queue의 뒤에서 요소를 추가하고 앞에서 요소를 제거하는 방식으로 동작한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;329&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xskPh/btsMbBfxrU7/o9BIq2fNcIS2sajTJ90MBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xskPh/btsMbBfxrU7/o9BIq2fNcIS2sajTJ90MBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xskPh/btsMbBfxrU7/o9BIq2fNcIS2sajTJ90MBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxskPh%2FbtsMbBfxrU7%2Fo9BIq2fNcIS2sajTJ90MBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;329&quot; height=&quot;472&quot; data-origin-width=&quot;329&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;offer(E element) : 큐의 끝에 요소를 추가하며, 추가에 성공하면 true를 반환한다. (용량 제한이 있는 경우, 공간이 부족하면 false 반환)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;poll() : 큐의 맨 앞 요소를 제거하고 반환한다. 큐가 비어 있으면 null을 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;peek() : 큐의 맨 앞 요소를 제거하지 않고 반환한다. 큐가 비어 있으면 null을 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;isEmpty() : 큐가 비어 있는지 확인한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;size() : 큐에 있는 요소의 개수를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음 그림과 같이 Queue 인터페이스 역시 Collection의 자식이다. 대표적인 구현체는 ArrayDeque, LinkedList가 있다.&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_IMG_1702.jpeg&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;1210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d8tM24/btsMc0elsgK/JhkcDU6EPrvnNj2w9r3V21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d8tM24/btsMc0elsgK/JhkcDU6EPrvnNj2w9r3V21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d8tM24/btsMc0elsgK/JhkcDU6EPrvnNj2w9r3V21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd8tM24%2FbtsMc0elsgK%2FJhkcDU6EPrvnNj2w9r3V21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;864&quot; height=&quot;1210&quot; data-filename=&quot;edited_IMG_1702.jpeg&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;1210&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; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Deque&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt;Deque(Double Ended Queue)는 양쪽 끝에서 요소를 추가하거나 제거할 수 있는&amp;nbsp; 유연한 자료구조다!&lt;/i&gt;&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;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Deque를 사용하면 스택 연산과 &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;1332&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VKwBt/btsMagvftnz/l3oMbD2pM7YxfQbVkvRq30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VKwBt/btsMagvftnz/l3oMbD2pM7YxfQbVkvRq30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VKwBt/btsMagvftnz/l3oMbD2pM7YxfQbVkvRq30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVKwBt%2FbtsMagvftnz%2Fl3oMbD2pM7YxfQbVkvRq30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1332&quot; height=&quot;218&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;218&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;offerFirst() : 앞에 추가한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;offerLast() : 뒤에 추가한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;pollFirst() : 앞에서 꺼낸다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;pollLast() : 뒤에서 꺼낸다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Stack을 위한 메서드&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;push() : 앞에서 추가한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;pop() : 앞에서 꺼낸다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Queue를 위한 메서드&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;offer() : 뒤에서 추가한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;poll() : 앞에서 꺼낸다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Deque의 대표적인 구현체로 ArrayDeque와 LinkedList가 있다. &lt;b&gt;ArrayDeque는 동적 크기의 배열을 사용하며, 일반적으로 LinkedList보다 더 빠른 성능을 제공&lt;/b&gt;한다. 두 구현체 모두 앞, 뒤에서의 삽입 및 삭제 연산에서 평균적으로 O(1)의 성능을 보이지만, ArrayDeque는 연속적인 메모리 블록을 사용하여 캐시 적중률이 높아 실제 환경에서는 LinkedList보다 더 나은 성능을 보여준다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;❓&lt;i&gt;&quot; ArrayDeque는 추가로 특별한 원형 큐 자료 구조를 사용하는데, 덕분에 앞, 뒤 입력 모두 O(1)의 성능을 제공한다.&quot;&lt;/i&gt;&amp;nbsp; 라고 교안에 나와있는데, &lt;span style=&quot;background-color: #fafafa; text-align: start;&quot;&gt;ArrayDeque는 고정된 크기의 배열을 사용하는게 아닌가? 하는 의문이 들어 좀 더 찾아봤다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&amp;rarr; 여기서 말하는 '&lt;span style=&quot;background-color: #fafafa; text-align: start;&quot;&gt;원형 큐 자료 구조를 사용한다'라는 말은, 원형 큐처럼 동작한다는 의미인 것 같다.&lt;/span&gt;&lt;span style=&quot;background-color: #fafafa; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; 내부적으로 배열을 기반으로 하면서도 처음과 끝에서 삽입/삭제가 가능하도록 인덱스를 조정하는 구조이기 때문에 원형 큐처럼 보이는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; 여기서 인덱스를 조정하는 구조란, 두 개의 인덱스(head, tail) 사용해 원형 큐처럼 인덱스를 순환하면서 관리하는 구조를 말한다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Iterable &amp;amp; Iterator&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt;자바는 모든 자료구조를 동일한 방법으로 편리하게 순회할 수 있도록 Iterable과 Iterator 인터페이스를 제공한다!&lt;/i&gt;&lt;/u&gt;&lt;u&gt;&lt;i&gt;&lt;/i&gt;&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;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ Iterable(반복 가능한) 인터페이스의 주요 메서드 &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738989981656&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Iterable&amp;lt;T&amp;gt; {
    Iterator&amp;lt;T&amp;gt; iterator();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; Iterator 반복자를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 대부분의 컬렉션이 Iterable을 구현하고 있어 for-each문을 사용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; &lt;span style=&quot;text-align: start;&quot;&gt;✔️ Iterator(반복자)&lt;/span&gt;&amp;nbsp;인터페이스의 주요 메서드 &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738989994198&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Iterator&amp;lt;E&amp;gt; {
    boolean hasNext();
    E next();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;컬렉션을 탐색(반복)하는 방법을 제공한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; hasNext()&lt;/b&gt; : 다음 요소가 있는지 확인한다. 다음 요소가 없으면 false 를 반환한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;next()&lt;/b&gt; : 다음 요소를 반환한다. 내부에 있는 위치를 다음으로 이동한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 자료구조마다 Iterator의 구현 방식은 다르지만, 우리는 Iterator 인터페이스를 통해 동일한 방식으로 요소를 순회할 수 있다. 직접 구현할 필요 없이, 컬렉션 프레임워크가 제공하는 iterator() 메서드로 Iterator 객체를 가져와 사용하면 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ 향상된 for문(for-each)&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;향상된 for문을 사용하려면 Array 혹은 Iterable을 가지고 있어야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738994258549&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (int i : arr) {
    System.out.println(i);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위와 같이 향상된 for문을 쓰면 자바는 컴파일 시점에 다음과 같이 코드를 변경한다. 두 코드는 같은 코드이지만, 향상된 for문이 더 깔끔하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738994430208&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while (iterator.hasNext()) {
    Integer i = iterator.next();
    System.out.println(i);
}&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 자바 Collection 인터페이스의 상위에 Iterable이 있다는 것에 주목하자!&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;1539&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqcBkT/btsMbu12J1h/adJSmWOwq4b6RkD4TJIk8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqcBkT/btsMbu12J1h/adJSmWOwq4b6RkD4TJIk8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqcBkT/btsMbu12J1h/adJSmWOwq4b6RkD4TJIk8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqcBkT%2FbtsMbu12J1h%2FadJSmWOwq4b6RkD4TJIk8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1539&quot; height=&quot;778&quot; data-origin-width=&quot;1539&quot; data-origin-height=&quot;778&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;하지만 Map은 키-값 구조이기 때문에 Iterable 대신 Map 인터페이스가 상위에 존재한다. 따라서 Map 자체를 Iterable을 통해 순회할 수는 없지만, entrySet(), keySet(), values()를 사용해 Set이나 Collection을 반환받아 향상된 for문을 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Comparable &amp;amp; Comparator&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt;객체를 기본 정렬할 때 Comparable를 사용하고, 다른 정렬 기준이 필요하면 Comparator를 사용하자!&lt;/i&gt;&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;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;✔️ 배열 정렬&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Arrays.sort()&lt;/b&gt; : 배열에 들어있는 데이터를 순서대로 정렬한다.(오름차순)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1739032471514&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Integer[] array = {3, 2, 1};
System.out.println(Arrays.toString(array));

System.out.println(&quot;기본 정렬 후&quot;);
Arrays.sort(array);
System.out.println(Arrays.toString(array));&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1739032487415&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[3, 2, 1]
기본 정렬 후
[1, 2, 3]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Arrays.sort(array, Comparator)&lt;/b&gt; : 내림차순 정렬이나 특정 기준을 적용한 정렬을 할 때 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이 &lt;span style=&quot;text-align: left;&quot;&gt;인수로 비교자(Comparator)를 만들어 넘겨주고 정렬 방식을 지정한다. 두 인수를 비교해 경과 값을 반환한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739033000878&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Comparator&amp;lt;T&amp;gt; {
    int compare(T o1, T o2);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 첫 번째 인수가 더 작으면 음수(작은 값이 앞쪽으로 이동)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;두 값이 같으면 0 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;강의 실습을 통해 다음과 같이 AscComparator와 DescComparator를 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739032877015&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Comparator;

public class AscComparator implements Comparator&amp;lt;Integer&amp;gt; {
    @Override
    public int compare(Integer o1, Integer o2) {
        return (o1 &amp;lt; o2) ? -1 : ((o1 == o2) ? 0 : 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; DescComparator는 AscComparator과 거의 같지만 마지막에 -1을 곱해줘서 정렬의 결과가 오름차순의 반대인 내림차순이 다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739032981401&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Comparator;

public class DescComparator implements Comparator&amp;lt;Integer&amp;gt; {
    @Override
    public int compare(Integer o1, Integer o2) {
        return ((o1 &amp;lt; o2) ? -1 : ((o1 == o2) ? 0 : 1)) * -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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이 사용할 수 있다. 맨 마지막과 같이 reversed() 메서드를 사용하면 비교의 결과를 반대로 변경한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739033125395&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Integer[] array = {3, 2, 1};

Arrays.sort(array, new AscComparator()); // [1, 2, 3]
Arrays.sort(array, new DescComparator()); // [3, 2, 1]
Arrays.sort(array, new AscComparator().reversed()); // [3, 2, 1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;직접 만든 객체를 정렬하려면 객체에 비교 기능을 추가해주는 Comparable 인터페이스를 구현하면 된다. 자기 자신과 인수로 넘어온 객체를 비교해서 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739036121694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Comparable&amp;lt;T&amp;gt; {
    public int compareTo(T o);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 현재 객체가 인수로 주어진 객체보다 더 작으면 음수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;두 객체의 크기가 같으면 0 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;현재 객체가 인수로 주어진 객체보다 더 크면 양수&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Comparable을 구현한 MyUser 클래스를 보자. 정렬의 기준을 나이로 정해 compareTo()를 구현했다. 이후 MyUser로 구성된 배열을 Arrays.sort()에 넣으면 객체 스스로 가지고 있는 Comparable을 사용해 비교한다. 따라서 나이 기준 오름차순으로 정렬된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739036177785&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MyUser implements Comparable&amp;lt;MyUser&amp;gt; {

    private String id;
    private int age;

    public MyUser(String id, int age) {
        this.id = id;
        this.age = age;
    }

    public String getId() {
        return id;
    }

    public int getAge() {
        return age;
    }

    @Override
    public int compareTo(MyUser o) {
        return this.age &amp;lt; o.age ? -1 : (this.age == o.age ? 0 : 1);
    }

    @Override
    public String toString() {
        return &quot;MyUser{&quot; +
                &quot;id='&quot; + id + '\'' +
                &quot;, age=&quot; + age +
                '}';
    }
}&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;만약 나이가 아니라 아이디로 비교하고 싶다면 아이디로 비교할 수 있는 IdComparator를 만들어 Array.sort의 인수로 넘겨주면 된다. 그러면 MyUser 객체가 기본으로 가지고 있는 Comparable을 무시하고 전달된 비교자를 사용해 정렬한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739036471030&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class IdComparator implements Comparator&amp;lt;MyUser&amp;gt; {
    @Override
    public int compare(MyUser o1, MyUser o2) {
        return o1.getId().compareTo(o2.getId());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Comparable을 활용한 기본 정렬&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1739073724980&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public int compareTo(MyUser other) {
    int ageCompare = Integer.compare(this.age, other.age); // 나이 기준 정렬
    if (ageCompare == 0) {
        return this.id.compareTo(other.id); // 같은 나이면 ID 기준 정렬 (문자열 오름차순)
    }
    return ageCompare;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Comparator를 활용한 정렬&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;b&gt;여러 개의 정렬 기준을 적용해야 할 때&lt;/b&gt; Comparator의 .thenComparing()을 활용하면 더 코드가 짧고 가독성이 좋아진자.&amp;nbsp; &lt;span style=&quot;background-color: #fafafa; text-align: start;&quot;&gt;thenComparing()은 &lt;/span&gt;&lt;span style=&quot;background-color: #fafafa; text-align: start;&quot;&gt;여러 개의 정렬 기준을 쉽게 조합할 수 있는 메서드다.&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739090850091&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 나이 오름차순, 같은 나이면 ID 오름차순
Collections.sort(users, 
    Comparator.comparing(MyUser::getAge)
              .thenComparing(MyUser::getId)
);&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;만약 나이를 내림차순으로 정렬하고자 한다면, MyUser::getAge 뒤에 Comparator.reverseOrder()를 적용하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;✔️ 컬렉션 정렬&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위에서 만든 MyUser 객체를 리스트에 담아 정렬한다면 다음과 같이 사용할 수 있다. 여기서 객체 스스로 정렬 메서드를 가지고 있는 list.sort() 사용을 더 권장한다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;list.sort() (권장)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; list.sort(new Comparator()) (권장)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Collections.sort(list)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Collections.sort(list, Comparator())&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1739090419359&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기본 정렬
list.sort(null);
Collections.sort(list);

// IdComparator 정렬
list.sort(new IdComparator());
Collections.sort(list, new IdComparator());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;✔️ TreeSet, TreeMap은 Comparable 혹은 Comparator 필수!&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;TreeSet이나 TreeMap은 이진 탐색 트리로 데이터를 정렬하면서 보관하기 때문에 정렬 기준을 꼭 제공해줘야 한다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;별도의 비교자를 제공하지 않으면 객체가 구현한 &lt;span style=&quot;text-align: start;&quot;&gt;Comparable을 사용해 기본(자연) 순서로 정렬하고, 별도의 비교자를 제공하면 해당 Comparator를 사용해 정렬한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739071660696&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;new TreeSet&amp;lt;&amp;gt;() // Comparable 사용
new TreeSet&amp;lt;&amp;gt;(new IdComparator()) // Comparator 사용&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ Collections 정렬 관련 메서드&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;max: 정렬 기준으로 최대 값을 찾아서 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;min: 정렬 기준으로 최소 값을 찾아서 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;shuffle: 컬렉션을 랜덤하게 섞는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;sort: 정렬 기준으로 컬렉션을 정렬한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;reverse: 정렬 기준의 반대로 컬렉션을 정렬한다. (컬렉션에 들어있는 결과를 반대로 정렬한다.)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;b&gt;✔️ of() 메서드로 컬렉션 생성(권장)&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;List.of(), Set.of(), Map.of()를 사용하면 불변 컬렉션을 반환므로 요소를 추가, 삭제, 변경할 수 없다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738985807353&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; list = List.of(1,2,3);
Set&amp;lt;Integer&amp;gt; set = Set.of(1,2,3);
Map&amp;lt;Integer,String&amp;gt; map = Map.of(1,&quot;one&quot;,2,&quot;two&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;b&gt;✔️ Arrays.asList() 메서드로 리스트 생성&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 고정 크기 리스트를 반환하므로, 요소를 추가하거나 삭제할 수 없지만,&amp;nbsp; 기존 요소들은 변경할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738987553571&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; list = Arrays.asList(1, 2, 3);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 내부적으로 배열을 기반으로 하는 리스트이므로, asList()로 만든 리스트는 원본 배열과 연결된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;기존 배열의 참조값을 그대로 활용하기 때문에 생성 비용이 적고, 큰 자료구조가 필요한 경우 적합하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738987627307&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Integer[] arr = {1, 2, 3};
List&amp;lt;Integer&amp;gt; list = Arrays.asList(arr);
arr[0] = 10;  // 리스트에도 반영됨 &amp;rarr; list는 [10, 2, 3]&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ 불변 컬렉션 &amp;rarr; &amp;nbsp;가변 컬렉션 전환&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738986977154&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; list = new ArrayList&amp;lt;&amp;gt;(List.of(1, 2, 3));
Set&amp;lt;Integer&amp;gt; set = new HashSet&amp;lt;&amp;gt;(Set.of(1, 2, 3));
Map&amp;lt;Integer, String&amp;gt; map = new HashMap&amp;lt;&amp;gt;(Map.of(1, &quot;one&quot;, 2, &quot;two&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;b&gt;✔️ 가변 컬렉션&amp;nbsp;&amp;rarr;&amp;nbsp; 불변 컬렉션 전환&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Collections.unmodifiableXXX()&lt;/b&gt;를 사용해 가변 컬렉션을 불변 컬렉션으로 전환할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738987085734&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; mutableList = new ArrayList&amp;lt;&amp;gt;(List.of(1, 2, 3));
List&amp;lt;Integer&amp;gt; immutableList = Collections.unmodifiableList(mutableList);

Set&amp;lt;Integer&amp;gt; mutableSet = new HashSet&amp;lt;&amp;gt;(Set.of(1, 2, 3));
Set&amp;lt;Integer&amp;gt; immutableSet = Collections.unmodifiableSet(mutableSet);

Map&amp;lt;Integer, String&amp;gt; mutableMap = new HashMap&amp;lt;&amp;gt;(Map.of(1, &quot;one&quot;, 2, &quot;two&quot;));
Map&amp;lt;Integer, String&amp;gt; immutableMap = Collections.unmodifiableMap(mutableMap);&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;b&gt;✔️ 빈 가변 컬렉션 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738987302335&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; mutableList = new ArrayList&amp;lt;&amp;gt;();
Set&amp;lt;Integer&amp;gt; mutableSet = new HashSet&amp;lt;&amp;gt;();
Map&amp;lt;Integer, String&amp;gt; mutableMap = new HashMap&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;자바5부터 제공&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738987371567&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; emptyList = Collections.emptyList();
Set&amp;lt;Integer&amp;gt; emptySet = Collections.emptySet();
Map&amp;lt;Integer, String&amp;gt; emptyMap = Collections.emptyMap();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;자바9부터 제공(권장)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738987392345&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; emptyList = List.of();
Set&amp;lt;Integer&amp;gt; emptySet = Set.of();
Map&amp;lt;Integer, String&amp;gt; emptyMap = Map.of();&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Collections.synchronizedList()&lt;/b&gt;를 사용해 동기화 문제가 발생하지 않는 안전한 리스트를 만들 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 동기화 작업으로 인해 성능은 &lt;span style=&quot;text-align: left;&quot;&gt;일반 리스트보다 &lt;/span&gt;느리다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738988104277&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ArrayList&amp;lt;Integer&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
list.add(1);
list.add(2);
list.add(3);

List&amp;lt;Integer&amp;gt; synchronizedList = Collections.synchronizedList(list);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;드디어 자바 강의 기본편부터 중급1, 중급2편 완강!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;꾸준히 복습하면서 나아가자.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB0V82/btsMcG8n4ne/P8VOYlZb8Iniv2kfjwwAIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB0V82/btsMcG8n4ne/P8VOYlZb8Iniv2kfjwwAIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB0V82/btsMcG8n4ne/P8VOYlZb8Iniv2kfjwwAIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB0V82%2FbtsMcG8n4ne%2FP8VOYlZb8Iniv2kfjwwAIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;731&quot; height=&quot;874&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;874&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Java</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/366</guid>
      <comments>https://3uomlkh.tistory.com/366#entry366comment</comments>
      <pubDate>Fri, 7 Feb 2025 03:13:57 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring에서 @RequestBody를 사용할 때 주의해야 할 점</title>
      <link>https://3uomlkh.tistory.com/365</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;삭제 기능 테스트 도중, 올바른 비밀번호를 입력해도 404 에러가 뜨는 문제가 있었다. 404 에러를 던지도록 설정한 부분은 DB에 있는 비밀번호와 입력한 비밀번호가 틀리는 경우 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이것저것 시도해보다 Repository Layer에서 정확한&amp;nbsp;scheduleId와&amp;nbsp;password가 출력되는 것을 확인한 후, 비밀번호를 검증하는 Service Layer에서 다음과 같이 출력해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;System.out.println(&quot;db password = &quot; + current.getPassword());
System.out.println(&quot;input password = &quot; + password);
System.out.println(&quot;equals = &quot; + current.getPassword().equals(password));&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;그 결과는 다음과 같았다. JSON 형식의 문자열이 그대로 들어온 것이다.&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;274&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RN2kc/btsL7u7HyxS/gv4ITlkBT03SP5CvAZHLW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RN2kc/btsL7u7HyxS/gv4ITlkBT03SP5CvAZHLW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RN2kc/btsL7u7HyxS/gv4ITlkBT03SP5CvAZHLW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRN2kc%2FbtsL7u7HyxS%2Fgv4ITlkBT03SP5CvAZHLW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;274&quot; height=&quot;158&quot; data-origin-width=&quot;274&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Controller에서 보니 비밀번호를 String 그대로 넣은 것이 문제였다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@DeleteMapping(&quot;/{scheduleId}&quot;)
public ResponseEntity deleteSchedule(
        @PathVariable Long scheduleId,
        @RequestBody(required = false) String password
) {
    scheduleService.deleteSchedule(scheduleId, password);
    
    return new ResponseEntity&amp;lt;&amp;gt;(HttpStatus.NO_CONTENT);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;inputPassword&amp;nbsp;&amp;rarr;&amp;nbsp;{&quot;password&quot;: &quot;1111&quot;}&amp;nbsp;(JSON 객체)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;dbPassword&amp;nbsp;&amp;rarr;&amp;nbsp;&quot;1111&quot;&amp;nbsp;(String)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서&amp;nbsp;String&amp;nbsp;타입이 아닌&amp;nbsp;ScheduleRequestDto&amp;nbsp;타입으로 변경한 뒤&amp;nbsp;dto에서&amp;nbsp;getPassword()를 해서 비밀번호만 넘겨주었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@DeleteMapping(&quot;/{scheduleId}&quot;)
public ResponseEntity&amp;lt;Void&amp;gt; deleteSchedule(
        @PathVariable Long scheduleId,
        @RequestBody ScheduleRequestDto dto
) {
    scheduleService.deleteSchedule(scheduleId, dto.getPassword());
    return new ResponseEntity&amp;lt;&amp;gt;(HttpStatus.NO_CONTENT);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;image (7).png&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8ITVs/btsL6Vkq6pl/XB4KS2VTBqzkwaIhGXa6B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8ITVs/btsL6Vkq6pl/XB4KS2VTBqzkwaIhGXa6B1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8ITVs/btsL6Vkq6pl/XB4KS2VTBqzkwaIhGXa6B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8ITVs%2FbtsL6Vkq6pl%2FXB4KS2VTBqzkwaIhGXa6B1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;290&quot; height=&quot;107&quot; data-filename=&quot;image (7).png&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;107&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;또한 Postamn에서의 테스트도 성공하였다.&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;1280&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rZMj3/btsL5QxGjT3/VEfVNTf0Bas41aQSMY6wn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rZMj3/btsL5QxGjT3/VEfVNTf0Bas41aQSMY6wn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rZMj3/btsL5QxGjT3/VEfVNTf0Bas41aQSMY6wn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrZMj3%2FbtsL5QxGjT3%2FVEfVNTf0Bas41aQSMY6wn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;556&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이 과정을 통해 &lt;u&gt;&lt;b&gt;@RequestBody에서 JSON 데이터를 받을 때 String 대신 DTO 객체를 사용하는 것이 좋다&lt;/b&gt;&lt;/u&gt;는 것을 알게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;여기서 또 수정된 부분이 있는데,&amp;nbsp;required = false로 적은 부분을 삭제한 부분이다.&amp;nbsp;@RequestBody(required = true)&amp;nbsp;가 설정되어 있으면, 클라이언트는 JSON을 반드시 보내야 하지만, 특정 필드는&amp;nbsp;null이 될 수도 있다. 이때,&amp;nbsp;@RequestBody(required = false)로 변경하면 요청 본문이 없어도 예외가 발생하지 않는다. 비밀번호가 선택 사항이라면 false로 하는 것도 고려할 수 있지만 보통 비밀번호는 필수값이기 때문에 사용하지 않는다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/365</guid>
      <comments>https://3uomlkh.tistory.com/365#entry365comment</comments>
      <pubDate>Tue, 4 Feb 2025 15:53:17 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] queryForObject() vs query()</title>
      <link>https://3uomlkh.tistory.com/364</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;예외 처리를 구현하는 도중, 원하는 곳에서 예외가 발생하지 않고 엉뚱한 곳에서 발생해서 그 원인을 찾아보았다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;일정을 수정하는 부분 중&amp;nbsp;findScheduleByIdWithPassword()에서 내가 설정한&amp;nbsp;ScheduleNotFoundException이 아니라&amp;nbsp;EmptyResultDataAccessException이 났다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Transactional
@Override
public ScheduleResponseDto updateSchedule(Long scheduleId, ScheduleRequestDto dto) {
    Schedule current = scheduleRepository.findScheduleByIdWithPassword(scheduleId);

    if (current == null) {
        throw new ScheduleNotFoundException(&quot;수정할 일정이 존재하지 않습니다.&quot;);
    }

	// 중략..

}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해당 메서드가 구현되어있는 Repository로 가서 살펴보니, 다음과 같이&amp;nbsp;queryForObject()를 사용하고 있는 것이 문제였다.&amp;nbsp;queryForObject()는 결과가 없으면&amp;nbsp;null을 반환하는 게 아니라 예외를 던지기 때문에, Service에서&amp;nbsp;null&amp;nbsp;체크를 할 수 없던 것이었다!&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Override
public Schedule findScheduleByIdWithPassword(Long scheduleId) {
    return jdbcTemplate.queryForObject(
            &quot;SELECT * FROM schedules WHERE scheduleId = ?&quot;,
            scheduleWithPasswordRowMapper(),
            scheduleId
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서&amp;nbsp;queryForObject()&amp;nbsp;대신&amp;nbsp;query()를 사용하여 예외를 발생시키지 않고, 결과가 없으면&amp;nbsp;null을 반환하도록 수정했다. 그리고 Service에서 null 체크 후 사용자 정의 예외인&amp;nbsp;ScheduleNotFoundException을 던지는 것에 성공했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Override
public Schedule findScheduleByIdWithPassword(Long scheduleId) {
    String query = &quot;SELECT * FROM schedules WHERE scheduleId = ? AND is_deleted = FALSE&quot;;
    List&amp;lt;Schedule&amp;gt; schedules = jdbcTemplate.query(query, scheduleWithPasswordRowMapper(), scheduleId);
    return schedules.isEmpty() ? null : schedules.get(0);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Postman에서도 내가 원하는 대 상태코드와 메세지가 출력되는 것을 볼 수 있다.&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;1280&quot; data-origin-height=&quot;787&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUoVdw/btsL50NMoR8/bxz4OmAr8M9ON0gU3STpoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUoVdw/btsL50NMoR8/bxz4OmAr8M9ON0gU3STpoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUoVdw/btsL50NMoR8/bxz4OmAr8M9ON0gU3STpoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUoVdw%2FbtsL50NMoR8%2Fbxz4OmAr8M9ON0gU3STpoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;787&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;787&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;✔️ 정리&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; queryForObject()&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;결과가 반드시 1개일 거라고 가정할 때 사용해야 하는 메서드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 조회된 결과가 0개면 EmptyResultDataAccessException 발생 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 조회된 결과가 2개 이상이면 IncorrectResultSizeDataAccessException 발생 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; query()&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 조회된 결과가 1개 이상이면 리스트로 반환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; queryForObject()와 다르게 결과가 없을 때 예외를 던지지 않음&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; 즉, &quot;조회된 결과가 없을 수도 있다&quot;면 query()를 쓰고, &quot;반드시 1개가 나와야 한다&quot;면 queryForObject()를 쓰자!&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/364</guid>
      <comments>https://3uomlkh.tistory.com/364#entry364comment</comments>
      <pubDate>Mon, 3 Feb 2025 23:36:12 +0900</pubDate>
    </item>
    <item>
      <title>[Java/Study] 김영한의 실전 자바 중급 2편 - 스터디 14회차</title>
      <link>https://3uomlkh.tistory.com/362</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #666666;&quot;&gt;인프런 강의 &amp;lt;김영한의 실전 자바 - 중급 2편&amp;gt;을 보고 정리한 내용입니다.&lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #666666;&quot;&gt;매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.&lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; HashSet&lt;/b&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt; 해시 자료 구조에 데이터를 저장하는 경우 hashCode() 를 구현해야 한다!&lt;/i&gt;&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;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해시 인덱스는 배열의 인덱스로 사용되기 때문에 양의 정수만 사용 가능하다. 따라서 문자 혹은 객체의 경우 &lt;span style=&quot;text-align: start;&quot;&gt;hashCode() 메서드를 통해 고유한 숫자를 만들고, 그 숫자를 이용해&lt;/span&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;문자열의 경우, hashCode() 메서드를 통해 문자를 기반으로 고유한 숫자를 만든다. 강의에서는 &lt;/span&gt;'A' - 65, 'B' - 66, 'AB' - 131와 같이 아스키 코드를 통해 각 문자에 고유한 숫자를 할당해 계산했다. 실제 자바의 String.hashCode()는 다음과 같이 각 문자에 가중치를 적용하여 더 복잡한 연산을 수행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737824722851&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;객체의 경우도 마찬가지로 &lt;span style=&quot;text-align: start;&quot;&gt;hashCode() 메서드를 통해 고유한 숫자를 만든다. Object 에 있는 hashCode() 메서드를 보통 오버라이딩해서 사용한다. 해당 메서드의 기본 구현은 객체의 참조값을 기반으로 해시 코드를 생성한다. 따라서 만약 id값으로 Member 객체를 구분하고 싶다고 하면, 반드시 hashCode()를 구현해줘야 제대로 구분할 수 있다. 구현하지 않으면 각 인스턴스 별로 해시코드가 다르기 때문에 같은 id라도 인스턴스가 다르면 제대로 구분되지 않는다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737824913660&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Object {
    public int hashCode();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;✔️ Object의 기본 기능&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;hashCode()&lt;/b&gt; : 객체의 참조값을 기반으로 해시 코드 반환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;equals()&lt;/b&gt; : == 동일성 비교를 해서 객체의 참조값이 같아야 true 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;❓hash()와 hashCode() 차이&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&amp;nbsp;강의에서는 hash()를 사용했는데, IDE에서 생성된 걸 보니 hashCode()로 생성되었다. 그 차이가 무엇인지 궁금해서 찾아보았다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737781480485&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private int hashIndex(Object value) {
    return Math.abs(value.hashCode() % capacity);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1737781500934&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private int hashIndex(Object value) {
    return Math.abs(value.hash() % capacity);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;hashCode()&lt;/b&gt; :&amp;nbsp; 객체 자체의 고유 해시코드를 반환하며, 해시 기반 자료구조에서 사용된다.(ex. id 기반으로 해시 코드 생성 시)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;Objects.hash()&lt;/b&gt; : 여러 필드를 기반으로 해시코드를 계산할 때 유용하며, 주로 hashCode() 메서드 구현을 단순화하는 데 사용된다.(ex. id와 name을 기반으로 해시 코드를 생성 시)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;equals, hashCode의 중요성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt;해시 자료 구조 사용 시 equals()와 hashCode()를 반드시 구현해야 한다!&lt;/i&gt;&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해시 자료 구조를 사용할 때 hashCode() 뿐만 아니라 해시 충돌을 대비해 equals() 역시 재정의가 필요하다. 해시 인덱스가 충돌하면 같은 해시 인덱스에 있는 데이터들을 하나하나 비교해야하는데, 이때 equals()를 사용하기 때문이다. 만약 특정 인덱스에 데이터가 하나만 있다고 하더라도 해시 인덱스와 실제 저장된 데이터는 다를 수 있기 때문에 equals()를 꼭 구현해야 한다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&quot;JPA&quot; 회원 / &quot;hi&quot; 회원의 해시 인덱스 : 0번&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&amp;nbsp;&quot;hi&quot; 회원만 저장했을때, &quot;JPA&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;equals()를 구현했을 경우 -&amp;gt; 0번 인덱스에는 &quot;hi&quot; 회원만 존재하므로 검색 실패&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;equals()를 구현하지 않았을 경우 -&amp;gt; 0번 인덱스에 있는 객체를 무작정 찾아서 &quot;hi&quot; 회원이 검색되는 오류 발생&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이런 식으로 오류가 발생할 수 있는데, Object가 기본으로 제공하는 기능은 단순히 인스턴스의 참조값을 기반으로 작동하기 때문이다. 강의에서는 다음과 같이 세 가지 상황에서 어떤 결과가 나오는지 알아보았다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;hashCode, equals를 모두 구현하지 않은 경우&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;데이터 저장 문제 : Object의 기본 기능을 사용해 인스턴스 참조값 기반으로 해시 코드를 생성했기 때문에 같은 id라도 다른 위치에 저장되어 중복 저장되는 문제 발생&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;데이터 검색 문제 : 데이터 검색 시 사용되는 해시 코드 역시 달라서 검색 실패&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;hashCode는 구현했지만 &lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;equals를 구현하지 않은 경우&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터 저장 문제 : &lt;span style=&quot;text-align: start;&quot;&gt;hashCode() 재정의로 같은 id라면 &lt;/span&gt;같은 해시 코드를 사용, &amp;nbsp;그러나 중복 데이터 체크 로직에서 사용되는 contains()는 내부에서 equals()를 사용하므로 인스턴스의 참조값 기반으로 중복 체크&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터 검색 문제 : contains() 내부에서 equals()로 비교하기 때문에 인스턴스 참조값을 비교하므로 검색 실패&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;hashCode,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;equals를 모두 구현한 경우 : 같은 id라면 같은 해시 코드 사용, 중복 데이터 체크 성공, 검색 성공&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해시 함수 : 임의의 길이릐 데이터를 입력받아 고정된 길이(=저장 공간의 크기)의 해시 코드를 출력하는 함수.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해시 충돌 : 다른 데이터를 입력해도 같은 해시 코드가 출력되는 것.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해시 코드 : 해시 함수를 통해서 나온 결과로, 데이터를 대표하는 값.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해시 인덱스 : 해시 코드를 사용해 데이터의 저장 위치를 결정하는 값.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Set&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt;Set은 중복을 허용하지 않고, 순서를 보장하지 않는 자료구조로, 빠른 검색이 가능하다!&lt;/i&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;u&gt;&lt;i&gt;실무에서는 주로 HashSet을 사용하며, 상황에 따라 LinkedHashSet(입력 순서 보장), TreeSet(데이터 값 기준 정렬)을 사용한다!&lt;/i&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;HashSet&amp;nbsp; / &lt;/b&gt;&lt;b&gt;LinkedHashSet&amp;nbsp; / &lt;/b&gt;&lt;b&gt;TreeSet&amp;nbsp; 요약&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250125_001505698.jpg&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3eq40/btsL1fqChlw/c28Z8mwvfR3U6KM4bj24C0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3eq40/btsL1fqChlw/c28Z8mwvfR3U6KM4bj24C0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3eq40/btsL1fqChlw/c28Z8mwvfR3U6KM4bj24C0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3eq40%2FbtsL1fqChlw%2Fc28Z8mwvfR3U6KM4bj24C0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1115&quot; height=&quot;1440&quot; data-filename=&quot;KakaoTalk_20250125_001505698.jpg&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ Set 인터페이스의 주요 메서드 &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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;add(E e) : 지정된 요소를 세트에 추가한다.(이미 존재하는 경우 추가X)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;addAll(Collection&amp;lt;? extends E&amp;gt; c) : 지정된 컬렉션의 모든 요소를 세트에 추가한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;contains(Object o) : 세트가 지정된 요소를 포함하고 있는지 여부를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;containsAll(Collection&amp;lt;?&amp;gt; c) : 세트가 지정된 컬렉션의 모든 요소를 포함하고 있는지 여부를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;remove(Object o) : 지정된 요소를 세트에서 제거한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;removeAll(Collection&amp;lt;?&amp;gt; c) : 지정된 컬렉션에 포함된 요소를 세트에서 모두 제거한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;retainAll(Collection&amp;lt;?&amp;gt; c) : 지정된 컬렉션에 포함된 요소만을 유지하고 나머지 요소는 세트에서 제거한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;clear() : 세트에서 모든 요소를 제거한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;size() : 세트에 있는 요소의 수를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;isEmpty() : 세트가 비어 있는지 여부를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;iterator() : 세트의 요소에 대한 반복자를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;toArray() : 세트의 모든 요소를 배열로 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;toArray(T[] a) : 세트의 모든 요소를 지정된 배열로 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;✔️&lt;/span&gt; 재해싱(rehashing)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터의 양이 75% 이상일 경우, 배열의 크기를 2배 증가시키고 모든 데이터의 해시 인덱스를 커진 배열에 맞춰 다시 계산하는 과정. 자바는 재해싱을 통해 최적화 한다.&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;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt; &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000; text-align: start;&quot;&gt;✔️&lt;/span&gt; 이진 탐색 트리&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터를 입력하는 시점에 &lt;b&gt;데이터의 값을 기준으로 정렬&lt;/b&gt;해서 보관&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;한 번 비교를 할 때마다 절반의 데이터만 남길 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;O(log N)&lt;/b&gt;의 시간 복잡도&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;최악의 경우 O(n)의 시간 복잡도&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;TreeSet은 &lt;b&gt;레드-블랙 트리&lt;/b&gt;를 사용해 균형 유지, 따라서 최악의 경우에도 O(log N)의 시간 복잡도&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터를 차례로 순회하기 위해 &lt;b&gt;중위 순회&lt;/b&gt;(왼쪽 서브트리 -&amp;gt; 현재 노드 -&amp;gt; 오른쪽 서브트리)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_1699.png&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqDQVw/btsL1A8P3sL/hp33Zfg1p0VPgWkixjYMeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqDQVw/btsL1A8P3sL/hp33Zfg1p0VPgWkixjYMeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqDQVw/btsL1A8P3sL/hp33Zfg1p0VPgWkixjYMeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqDQVw%2FbtsL1A8P3sL%2Fhp33Zfg1p0VPgWkixjYMeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2400&quot; height=&quot;1600&quot; data-filename=&quot;IMG_1699.png&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1600&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이 트리가 불균형해지면 데이터 수만큼 검색을 해야한다. 따라서 최악의 경우 O(n)의 성능이 나온다.&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;IMG_1700.png&quot; data-origin-width=&quot;2039&quot; data-origin-height=&quot;1694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ptZJA/btsL1BNqE5i/Zz5iJexlsgaik1tkk5smgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ptZJA/btsL1BNqE5i/Zz5iJexlsgaik1tkk5smgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ptZJA/btsL1BNqE5i/Zz5iJexlsgaik1tkk5smgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FptZJA%2FbtsL1BNqE5i%2FZz5iJexlsgaik1tkk5smgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2039&quot; height=&quot;1694&quot; data-filename=&quot;IMG_1700.png&quot; data-origin-width=&quot;2039&quot; data-origin-height=&quot;1694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 TreeSet은 불균형을 해결하기 위해 다음과 같이 레드-블랙 트리를 사용한다. 위의 불균형한 트리를 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;IMG_1701.png&quot; data-origin-width=&quot;2039&quot; data-origin-height=&quot;1690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oxtte/btsL1WD4Maz/t8npAdUpkWqozCFd3Ad7Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oxtte/btsL1WD4Maz/t8npAdUpkWqozCFd3Ad7Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oxtte/btsL1WD4Maz/t8npAdUpkWqozCFd3Ad7Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foxtte%2FbtsL1WD4Maz%2Ft8npAdUpkWqozCFd3Ad7Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2039&quot; height=&quot;1690&quot; data-filename=&quot;IMG_1701.png&quot; data-origin-width=&quot;2039&quot; data-origin-height=&quot;1690&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ Set에 배열 편하게 넣기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Set 구현체의 생성자에 컬렉션을 전달할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;314&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1XGC8/btsL1tWklwA/Jwonw2C3YzISBYked4sJt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1XGC8/btsL1tWklwA/Jwonw2C3YzISBYked4sJt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1XGC8/btsL1tWklwA/Jwonw2C3YzISBYked4sJt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1XGC8%2FbtsL1tWklwA%2FJwonw2C3YzISBYked4sJt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;314&quot; height=&quot;205&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;314&quot; data-origin-height=&quot;205&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;따라서 다음과 같이 Integer 배열을 List로 바꿔 생성자에 넣으면 바로 추가된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737774868679&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Integer[] inputArr = {30, 20, 20, 10, 10};
Set&amp;lt;Integer&amp;gt; set = new LinkedHashSet&amp;lt;&amp;gt;(List.of(inputArr));&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;✔️ Arrays.asList()와 &lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;List.of()&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Arrays.asList()와 List.of() 모두 자바에서 List 객체를 생성하는 데 사용된다. &lt;span style=&quot;text-align: start;&quot;&gt;자바 9 이상을 사용한다면 List.of()를 권장한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;List.of()를 권장하는 이유가 궁금해 &lt;/span&gt;Arrays.asList()와&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;List.of()를 비교해보았다. List.of()를 사용하면 불변 컬렉션을 생성해서 데이터를 안전하게 보호할 수 있고, 가변 크기를 지원한다. 또한 내부적으로 더 효율적인 방식으로 동작한다고 한다. 가볍게 보고 넘어가자!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Arrays_vs_List_fully_refined.png&quot; data-origin-width=&quot;3965&quot; data-origin-height=&quot;2370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHNDzI/btsL1rxvXjQ/Xaz4SgfWpKSPchzGHvJP2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHNDzI/btsL1rxvXjQ/Xaz4SgfWpKSPchzGHvJP2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHNDzI/btsL1rxvXjQ/Xaz4SgfWpKSPchzGHvJP2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHNDzI%2FbtsL1rxvXjQ%2FXaz4SgfWpKSPchzGHvJP2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3965&quot; height=&quot;2370&quot; data-filename=&quot;Arrays_vs_List_fully_refined.png&quot; data-origin-width=&quot;3965&quot; data-origin-height=&quot;2370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>HashSet</category>
      <category>java</category>
      <category>LinkedHashSet</category>
      <category>treeset</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/362</guid>
      <comments>https://3uomlkh.tistory.com/362#entry362comment</comments>
      <pubDate>Sat, 25 Jan 2025 14:08:51 +0900</pubDate>
    </item>
    <item>
      <title>[Java/Study] 김영한의 실전 자바 중급 2편 - 스터디 13회차</title>
      <link>https://3uomlkh.tistory.com/360</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #666666;&quot;&gt;인프런 강의 &amp;lt;김영한의 실전 자바 - 중급 2편&amp;gt;을 보고 정리한 내용입니다.&lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #666666;&quot;&gt;매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.&lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&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;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; List&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&quot;의존한다&quot;&lt;/b&gt;&amp;nbsp;라는 것은 무엇일까? 다음과 같이 MyList를 구현하는 MyArrayList와 MyArrayList를 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface MyList&amp;lt;E&amp;gt; {
    int size();
    void add(E e);
    void add(int index, E e);
    E get(int index);
    E set(int index, E element);
    E remove(int index);
    int indexOf(E o);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class MyArrayList&amp;lt;E&amp;gt; implements MyList&amp;lt;E&amp;gt; {
 //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class MyLinkedList&amp;lt;E&amp;gt; implements MyList&amp;lt;E&amp;gt; {
 //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;만약 MyArrayList 를 활용해서 많은 데이터를 처리하는 BatchProcessor 클래스를 개발한다고 할 때, 데이터를 앞에서 추가하는 일이 많은 상황이 올 수 있다. 그러면 그때마다 MyLinkedList 를 사용하도록 코드를 변경해야 한다. 이런 상황을&amp;nbsp;BatchProcessor가 &lt;b&gt;&lt;u&gt;구체적인 클래스에 의존한다&lt;/u&gt;&lt;/b&gt;고 표현한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class BatchProcessor {

    private final MyArrayList&amp;lt;Integer&amp;gt; list = new MyArrayList&amp;lt;&amp;gt;();

    public BatchProcessor(MyList&amp;lt;Integer&amp;gt; list) {
        this.list = list;
    }

    public void logic(int size) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i &amp;lt; size; i++) {
            list.add(0, i); // 앞에 추가
        }
        long endTime = System.currentTimeMillis();
        System.out.println(&quot;크기: &quot; + size + 
        &quot;, 계산 시간: &quot; + (endTime - startTime) + &quot;ms&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;구체적인 클래스에 의존하는 대신 &lt;u&gt;&lt;b&gt;추상적인 MyList 인터페이스에 의존&lt;/b&gt;&lt;/u&gt;할 수 있다. 이렇게 하면 BatchProcessor를 생성하는 시점에 리스트 전략을 선택해 전달하면 되고, 클라이언트 코드인 BatchProcessor의 변경 없이도 MyArrayList에서 MyLinkedList로 변경하는 것이 가능하다. 바로 이것을&amp;nbsp;&lt;u&gt;&lt;b&gt;의존 관계 주입(Dependency Injection, DI)&lt;/b&gt;&lt;/u&gt;이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class BatchProcessor {

    private final MyList&amp;lt;Integer&amp;gt; list = new MyList&amp;lt;&amp;gt;();

    public BatchProcessor(MyList&amp;lt;Integer&amp;gt; list) {
        this.list = list;
    }

    public void logic(int size) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i &amp;lt; size; i++) {
            list.add(0, i); // 앞에 추가
        }
        long endTime = System.currentTimeMillis();
        System.out.println(&quot;크기: &quot; + size + 
        &quot;, 계산 시간: &quot; + (endTime - startTime) + &quot;ms&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이 외부에서 MyArrayList를 쓸지, MyLinkedList를 쓸지 결정할 수 있다는 것이다. 이 예시 코드에서는 &lt;span style=&quot;background-color: #ffffff; text-align: justify;&quot;&gt;BatchProcessor의 생성자를 통해 의존관계를 주입했기 때문에 &lt;b&gt;생성자 의존관계 주입(생성자 주입)&lt;/b&gt;이라고 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737200024695&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BatchProcessorMain {
    public static void main(String[] args) {
//        MyArrayList&amp;lt;Integer&amp;gt; list = new MyArrayList&amp;lt;&amp;gt;();
        MyLinkedList&amp;lt;Integer&amp;gt; list = new MyLinkedList&amp;lt;&amp;gt;();

        BatchProcessor processor = new BatchProcessor(list);
        processor.logic(50_000);
    }
}&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;1) 컴파일 타임 의존관계&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;컴파일 타임(compile time): 코드 컴파일 시점&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;자바 컴파일러가 보는 의존관계로, 실행하지 않은 소스 코드에 정적으로 나타난다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;BatchProcessor 클래스를 보면 MyList 인터페이스에만 의존하고 있는 것을 알 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;1530&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QAJEb/btsLTpFq0Pl/hAFxoRhgUATxPMFzJ84mI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QAJEb/btsLTpFq0Pl/hAFxoRhgUATxPMFzJ84mI0/img.png&quot; data-alt=&quot;컴파일 타임 의존관계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QAJEb/btsLTpFq0Pl/hAFxoRhgUATxPMFzJ84mI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQAJEb%2FbtsLTpFq0Pl%2FhAFxoRhgUATxPMFzJ84mI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1530&quot; height=&quot;649&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컴파일 타임 의존관계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;2) 런타임 의존관계 &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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;런타임(runtime): 프로그램 실행 시점&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;실제 프로그램이 작동할 때 보이는 의존관계로, 프로그램이 실행될 때 인스턴스 간에 의존관계이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;847&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sjyMC/btsLRDd5nBu/J4ljlg3plhHu7UGLpaLu50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sjyMC/btsLRDd5nBu/J4ljlg3plhHu7UGLpaLu50/img.png&quot; data-alt=&quot;런타임 의존관계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sjyMC/btsLRDd5nBu/J4ljlg3plhHu7UGLpaLu50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsjyMC%2FbtsLRDd5nBu%2FJ4ljlg3plhHu7UGLpaLu50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;847&quot; height=&quot;490&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;490&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;BatchProcessor를 생성할 때 생성자를 통해 MyArrayList의 참조인 x001을 던져준다. list 필드가 x001을 세팅하면 BatchProcessor가 MyArrayList를 사용할 수 있게 된다. 따라서 MyArrayList에 대해 런타임 의존관계를 가지게 된다. 따라서 이후에 logic() 을 호출하면 MyArrayList 인스턴스를 사용하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;클라이언트 클래스는 컴파일 타임에 추상적인 것에 의존하고, 런타임에 의존 관계 주입을 통해 구현체를 주입받아 사용함으로써 다양한 이점을 얻을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ 전략 패턴(Strategy Pattern)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;전략 패턴은 중요한 디자인 패턴 중 하나로, 위의 예시 코드처럼 &lt;u&gt;&lt;b&gt;알고리즘을 클라이언트 코드의 변경 없이 쉽게 교체할 수 있는 패턴&lt;/b&gt;&lt;/u&gt;이다. MyList 인터페이스가 전략을 정의하는 인터페이스가 되고, 각각의 구현체인 MyArrayList , MyLinkedList 가 전략의 구체적인 구현이 된다. 그리고 전략을 클라이언트 코드인 BatchProcessor의 변경 없이 손쉽게 교체할 수 있다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;ArrayList와 LinkedList의 성능을 비교해보면 어떨까? 결론부터 말하자면 &lt;u&gt;&lt;b&gt;대부분의 경우 ArrayList가 성능상 유리&lt;/b&gt;&lt;/u&gt;하기 때문에 실무에서는 주로 ArrayList를 사용한다. 데이터를 앞쪽에서 추가/삭제하는 일이 많다면 LinkedList를 고려하는 것이 좋다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;직접 구현한 MyArrayList &amp;amp; MyLinkedList와 자바가 제공하는 ArrayList &amp;amp; LinkedList의 성능 차이는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xf0dQ/btsLQsPxNHc/BX66kZOjzKKmW1fhpkFjd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xf0dQ/btsLQsPxNHc/BX66kZOjzKKmW1fhpkFjd1/img.png&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;1139&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.2481%; margin-right: 10px;&quot; data-widthpercent=&quot;49.83&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xf0dQ/btsLQsPxNHc/BX66kZOjzKKmW1fhpkFjd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxf0dQ%2FbtsLQsPxNHc%2FBX66kZOjzKKmW1fhpkFjd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;776&quot; height=&quot;1139&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bExH74/btsLOKqyrpP/kVVxqWWEJIqABElZZEVvl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bExH74/btsLOKqyrpP/kVVxqWWEJIqABElZZEVvl1/img.png&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;1137&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.5891%;&quot; data-widthpercent=&quot;50.17&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bExH74/btsLOKqyrpP/kVVxqWWEJIqABElZZEVvl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbExH74%2FbtsLOKqyrpP%2FkVVxqWWEJIqABElZZEVvl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;1137&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;직접 구현한 List(왼) / 자바 List(오)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;직접 구현한 List와 자바가 제공하는 List는 계산 시간 차이는 있지만 ArrayList와 LinkedList의 성능 차이는 비슷하다. 다른 점이라면 Linked 리스트의 경우 뒤에 추가(삭제) 할 때 자바의 LinkedList가 O(1)의 성능으로 더 빠르다. 왜냐하면 자바가 제공하는 LinkedList는 첫번째 위치뿐만 아니라 마지막 위치까지 가지고 있기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 120px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;ArrayList&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;LinkedList&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 앞에 추가(삭제) &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; O(n) &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #ee2323;&quot;&gt; O(1) &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 평균 추가(삭제) &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; O(n) &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; O(n) &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 뒤에 추가(삭제) &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #ee2323;&quot;&gt; O(1) &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #ee2323;&quot;&gt; O(1) &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 인덱스 조회 &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #ee2323;&quot;&gt; O(1) &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; O(n) &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;검색&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; O(n) &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; O(n) &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;또한 데이터 추가 시 MyArrayList(평균 966ms)보다 자바의 ArrayList(평균 45ms)가 더 빠른 것을 알 수 있다. 그 이유는 자바의 ArrayList는 데이터를 추가할 때 &lt;b&gt;메모리 고속 복사&lt;/b&gt;를 사용하기 때문이다. 데이터가 아주 많은 경우와 시스템에 따른 차이를 제외하면 대략 O(n/10) 정도의 성능으로 추정하고, 상수를 제거하면 O(n)이 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;✔️ 결론&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이론과 달리 실제 성능은 요소의 순차적 접근 속도, 메모리 할당 및 해제 비용, CPU 캐시 활용도 등 다양한 요소에 의해 영향을 받는다. 또한 ArrayList가 데이터를 하나씩 미는 대신 메모리 고속 복사를 이용하기도 한다. 특히 ArrayList는 메모리 상에서 연속적으로 위치하기 때문에 CPU 캐시 효율이 좋고, 메모리 접근 속도가 빠르다. 반면, LinkedList 는 각 요소가 별도의 객체로 존재하고 다음 요소의 참조를 저장하기 때문에 CPU 캐시 효율이 떨어지고, 메모리 접근 속도가 상대적으로 느려질 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;=&amp;gt;&amp;nbsp;&lt;b&gt;&lt;i&gt; 배열 리스트가 성능상 유리하다!&lt;/i&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt; Hash&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;List와 Set의 차이는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;List : 순서를 유지하며, 중복을 허용하는 자료구조. 인덱스를 통해 각 요소에 접근할 수 있다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;ex. 장바구니 목록, 순서가 중요한 일련의 이벤트 목록.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;Set : 순서를 유지하지 않으며, 중복을 허용하지 않는 자료구조. 요소의 유무를 빠르게 확인할 수 있도록 최적화 되어있어서 빠른 조회가 가능하다.&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;ex. 회원 ID 집합, 고유한 항목의 집합.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;직접 구현한 Set은 다음과 같다. 데이터 추가, 삭제, 확인만 하면 간단하게 구현할 수 있다. 단, Set은 중복을 허용하지 않기 때문에 데이터를 추가할 때 contains() 메서드로 중복 여부를 체크해줘야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public class MyHashSetV0 {

    private int[] elementData = new int[10];
    private int size = 0;

    // O(n) - contains() 메서드 때문
    public boolean add(int value) {
        // 중복 값 체크
        if (contains(value)) {
            return false;
        }
        elementData[size] = value;
        size++;
        return true;
    }

    // O(n) - for문으로 하나하나 찾아야 함
    public boolean contains(int value) {
        for (int data : elementData) {
            if (data == value) {
                return true;
            }
        }
        return false;
    }

    public int size() {
        return size;
    }

    @Override
    public String toString() {
        return &quot;MyHashSetV0{&quot; +
                &quot;elementData=&quot; + Arrays.toString(Arrays.copyOf(elementData, size)) +
                &quot;, size=&quot; + 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이렇게 만든 Set은 데이터 추가, 검색 모두 O(n)으로 성능이 좋지 않다. 데이터를 추가할 때마다 중복 데이터가 있는지 체크하기 위해 Set의 전체 데이터를 확인해야 한다. 이런 문제는 &lt;b&gt;해시 알고리즘&lt;/b&gt;을 사용하면 해결할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 id=&quot;해시-알고리즘&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;해시 알고리즘&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;배열에서 데이터를 검색할 때는 하나하나 비교해야하기 때문에 O(n)의 시간이 걸리고, 배열의 장점인 인덱스를 사용할 수 없다. 왜냐하면 인덱스와 데이터의 값이 서로 다르기 때문이다. 인덱스로 검색을 하려면 다음 코드와 같이 데이터 값을 인덱스 번호로 사용하면 된다. 이렇게 하면 인덱스로 바로 데이터를 찾을 수 있어 O(1)의 시간이 걸린다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Integer[] inputArray = new Integer[10];
inputArray[1] = 1;
inputArray[2] = 2;
inputArray[5] = 8;
inputArray[8] = 8;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;위의 배열을 출력하면 이렇게 나올 것이다. 찾는 것은 바로 찾을 수 있겠지만, 수많은 null 값들이 매우 거슬린다. 만약 99라는 값이 들어간다면, 배열은 입력 값의 범위 만큼 큰 배열을 사용해야 한다. 따라서 낭비되는 공간이 많이 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;inputArray = [null, 1, 2, null, null, 5, null, null, 8, null]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이런 문제는&amp;nbsp;&lt;u&gt;&lt;b&gt;나머지 연산&lt;/b&gt;&lt;/u&gt;을 통해 해결할 수 있다. 배열의 크기에 맞추어 나머지 연산을 한 결과는 다음과 같다. 이렇게 하면 0 ~ 9 범위로 인덱스 값이 나온다. 이렇게 &lt;b&gt;배열의 인덱스로 사용할 수 있도록 원래의 값을 계산한 인덱스&lt;/b&gt;를 &lt;u&gt;&lt;b&gt;해시 인덱스(hashIndex)&lt;/b&gt;&lt;/u&gt;라 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;1 % 10 = 1&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;2 % 10 = 2&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;5 % 10 = 5&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;8 % 10 = 8&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;14 % 10 = 4&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;99 % 10 = 9&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;다음과 같이 나머지 연산을 하는&amp;nbsp;hashIndex()&amp;nbsp;메서드와 해시 인덱스를 이용해 값을 추가하는&amp;nbsp;add()&amp;nbsp;메서드를 구현했다. 넣을 때&amp;nbsp;hashIndex()&amp;nbsp;메서드를 통해 나온 값에 데이터를 넣고, 검색을 할 때도 검색하고자 하는 값을&amp;nbsp;hashIndex()&amp;nbsp;메서드에 넣어 해시 인덱스를 구하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;private static void add(Integer[] inputArray, int value) {
    int hashIndex = hashIndex(value);
    inputArray[hashIndex] = value;
}

static int hashIndex(int value) {
    return value % CAPACITY;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;하지만 여기에도 한계가 있다. 바로 &lt;u&gt;&lt;b&gt;해시 충돌&lt;/b&gt;&lt;/u&gt;이 날 수 있다는 것이다. 만약 9와 99를 입력한다면 둘 다 10으로 나눈 나머지가 9이기 때문에 덮어씌워져 마지막에 저장한 값만 남게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;99 % 10 = 9&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;9 % 10 = 9&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;이 문제를 해결하기 위해 배열의 크기를 늘리는 방법도 있겠지만 위에서 보았듯이 메모리 낭비가 심해질 것이다. 따라서 해시 충돌이 낮을 확률로 일어날 수 있다고 인정하고 같은&amp;nbsp;&lt;u&gt;&lt;b&gt;해시 인덱스의 값을 같은 인덱스에 저장&lt;/b&gt;&lt;/u&gt;하면 문제를 해결할 수 있다. 배열 안에 또 다른 배열(혹은 리스트)를 만들어 같은 해시 인덱스 값을 가진 데이터를 저장한다. 다음 코드는 LinkedList를 사용해 해시 충돌이 일어날 경우를 대비한 코드이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;private static void add(LinkedList&amp;lt;Integer&amp;gt;[] buckets, int value) {
    int hashIndex = hashIndex(value);
    LinkedList&amp;lt;Integer&amp;gt; bucket = buckets[hashIndex]; // O(1)
    if (!bucket.contains(value)) { // 중복 체크 - O(n)
        bucket.add(value);
    }
}

private static boolean contains(LinkedList&amp;lt;Integer&amp;gt;[] buckets, int searchValue {
    int hashIndex = hashIndex(searchValue);
    LinkedList&amp;lt;Integer&amp;gt; bucket = buckets[hashIndex]; // O(1)
    return bucket.contains(searchValue); // O(n)
}

static int hashIndex(int value) {
    return value % CAPACITY;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;데이터를 검색할 때는 LinkedList의&amp;nbsp;contains()&amp;nbsp;메서드를 이용해 모든 항목을 다 순회하면서 찾는 데이터가 있는지 확인한다. 따라서 O(n)의 성능이고, 해시 충돌이 발생하지 않아 데이터가 1개만 들어있는 경우에만 O(1)의 성능이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;  정리&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;해시 인덱스를 사용할 때 최악의 경우는 거의 발생하지 않고, 대부분 O(1)의 빠른 성능을 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;평균: O(1)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;최악: O(n)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;평균: O(1)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;최악: O(n)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java</category>
      <category>java</category>
      <category>배열리스트</category>
      <category>연결리스트</category>
      <category>인프런</category>
      <category>해시알고리즘</category>
      <author>개발곰발</author>
      <guid isPermaLink="true">https://3uomlkh.tistory.com/360</guid>
      <comments>https://3uomlkh.tistory.com/360#entry360comment</comments>
      <pubDate>Sat, 18 Jan 2025 22:38:03 +0900</pubDate>
    </item>
  </channel>
</rss>