<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>작은 발자국들의 위대한 여정</title>
    <link>https://thisisprogrammingworld.tistory.com/</link>
    <description>제가 깨달은 것을 정리하는 공간입니다.  </description>
    <language>ko</language>
    <pubDate>Sat, 22 Aug 2026 16:27:06 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>뭉지(moonz)</managingEditor>
    <image>
      <title>작은 발자국들의 위대한 여정</title>
      <url>https://tistory1.daumcdn.net/tistory/3165022/attach/2f59ae0edb6c44d58b6ce00f701a19ec</url>
      <link>https://thisisprogrammingworld.tistory.com</link>
    </image>
    <item>
      <title>엔티티 연관관계 (식별/비식별, Cardinality, 선택/필수)</title>
      <link>https://thisisprogrammingworld.tistory.com/255</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티 간 연관관계를 얘기할 때에는, &lt;b&gt;&quot;엔티티 간 식별 관계&quot;&lt;/b&gt; 와 &lt;b&gt;&quot;엔티티 간 연결되는 개체 수&quot;&lt;/b&gt;, &lt;b&gt;&quot;선택/필수 관계&quot;&lt;/b&gt;를 이용해서 관계를 정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 부모, 자식 관계가 있는 엔티티 간 &lt;b&gt;식별 여부&lt;/b&gt;를 정의하는 &quot;식별 관계&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 관계가 있는 엔티티 간 하나의 엔티티에서 다른 엔티티에 몇개의 개체와 &lt;b&gt;대응되는 갯수&lt;/b&gt;를 의미하는 &quot;Cardinality&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;대응되는 개체의 필수 여부&lt;/b&gt;를 정의하는 &quot;선택/필수 관계&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;식별 관계와 비식별 관계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 엔티티 간 식별 관계이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;식별/비식별 관계는, 자식 개체가 부모 객체 없이도 식별될 수 있는지, 부모 객체 없이는 식별될 수 없는지를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;식별 관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 엔티티가 부모 엔티티의 식별자 (PK)를 본인의 식별자 (PK)로 사용하거나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복합키일 경우, 자신의 식별자에 부모의 식별자를 포함시키는 경우 식별 관계로 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;자식 엔티티는 부모 엔티티 없이 식별될 수 없을 만큼, 개체 간 아주 강한 의존관계를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;예시&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1:M&lt;span&gt; 관계로, &lt;/span&gt;주문과 주문 상세 엔티티 관계가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 상세 엔티티는 주문의 PK인 주문번호를 자신의 PK로 갖기 때문에, 주문 데이터의 식별자 없이 주문 상세 데이터를 식별할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1:1 관계로, 조인 전략을 맺는 엔티티 간 관계를 예시로 들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김영한 님의 JPA 강의에서 만난 예제인데, 판매 상품 정보들의 공통점 데이터가 하나의 상품 테이블에 존재하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 ALBUM, MOVIE, BOOK 은 해당 ITEM 테이블을 상속받으면서, 각자의 속성을 관리하는 테이블이 있다. (객체의 상속 관계를 나타내는 '조인 전략' 방법이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 세 테이블은 ITEM 테이블의 PK 를 자신의 PK이자 FK로 참조하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2280&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MWa5L/btsPjyyN77I/JDP90CZGNbVKAi3i5j7GXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MWa5L/btsPjyyN77I/JDP90CZGNbVKAi3i5j7GXK/img.png&quot; data-alt=&quot;출처 : 김영한님의 &amp;amp;lt;자바 ORM 표준 JPA 프로그래밍 - 기본편 - 고급 매핑&amp;amp;gt; 강의 자료 중&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MWa5L/btsPjyyN77I/JDP90CZGNbVKAi3i5j7GXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMWa5L%2FbtsPjyyN77I%2FJDP90CZGNbVKAi3i5j7GXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2280&quot; height=&quot;680&quot; data-origin-width=&quot;2280&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 김영한님의 &amp;lt;자바 ORM 표준 JPA 프로그래밍 - 기본편 - 고급 매핑&amp;gt; 강의 자료 중&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;비식별 관계&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자식 엔티티가 부모 엔티티와 독립적인 식별자 (PK)를 갖고 있어&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&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;letter-spacing: 0px;&quot;&gt;&amp;lt;예시&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;멤버 테이블과 팀 테이블이 있다. 멤버 테이블은 팀 테이블의 식별자를 PK에 포함하지는 않고, FK로 갖는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;팀에 속해있지 않은(TEAM_SEQ를 FK로 갖지 않는) 멤버도 독립적으로 존재할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Cardinality&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;관계가 있는 엔티티 간 하나의 엔티티에서 다른 엔티티에 몇개의 개체와 대응되는 지에 따라 여러 종류가 존재한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 1:1 관계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 1:N 관계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. M:N 관계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;1:1 관계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 엔티티에서 다른 엔티티에 대해 하나의 개체만 대응되는 관계이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;사례&amp;gt;&amp;nbsp;회원과 회원 정보 테이블. 회원 데이터 한개는 하나의 회원 정보 테이블 데이터와 매핑되기 때문에 1:1 관계이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;1:N 관계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 엔티티에서 다른 엔티티에 대해 2개 이상의 개체와 대응되지만, 반대 방향으로는 단일 개체만 매칭되는 &amp;nbsp;관계이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;사례&amp;gt;&amp;nbsp;회원과 주문 테이블. 회원 데이터 한개는 2개 이상의 주문을 할 수 있지만, 주문 데이터 한개는 2개 이상의 회원과 연결될 수 없는, 1:N 관계이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;M:N 관계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 엔티티에서 다른 엔티티의 2개 이상의 개체와 대응되고, 반대 방향으로도 2개 이상의 개체와 대응되는 관계이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;사례&amp;gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주문과 상품 테이블. 주문 데이터 한개는 2개 이상의 상품 데이터를 갖을 수 있고, 상품 데이터 한개도 2개 이상의 주문 데이터와 대응될 수 있기 때문에 M:N 관계이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;i&gt;보통 M:N 관계는 1:M, N:1 관계로 풀어질 수 있도록 중간 테이블을 두는 것이 일반적이다. ex) 주문:상품 관계 (M:N)는 주문 : 주문_상품 = 1:M, 주문_상품 : 상품 = N:1 으로 구성.&lt;/i&gt;&lt;/span&gt;&lt;i&gt;&lt;/i&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;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;필수/선택 관계&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 엔티티 간에는 필수 관계, 선택 관계가 있다.&lt;/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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그렇기 때문에 외래키(FK) 에는 Null을 허용하지 않는다.&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이런 경우를 위해 외래키 (FK) 에 Null을 허용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&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;lt;사례&amp;gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1) 고객 (부모) 과 주문 (자식) 관계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주문 데이터는 반드시 고객 데이터를 가져야 하지만&amp;nbsp; -&amp;gt; 자식 엔티티에게는 필수 관계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;고객 데이터는 주문 데이터를 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;반드시&lt;span&gt; 가질 필요는 없다.&amp;nbsp; -&amp;gt; 부모 엔티티에게는 선택 관계&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;2) 할인 정책 (부모) 과 상품 (자식) 관계&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;상품은 할인 정책을 갖을 수 있지만, 필수는 아니다. -&amp;gt; 자식 엔티티에게는 선택 관계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;할인 정책은 하나 이상의 상품에 적용될 수도 있고, 아닐 수도 있다. -&amp;gt; 부모 엔티티에게도 선택 관계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&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;</description>
      <category>백엔드 개발하며 작성한/데이터베이스</category>
      <category>cardinality</category>
      <category>선택 관계</category>
      <category>식별 비식별 관계</category>
      <category>필수 관계</category>
      <author>뭉지(moonz)</author>
      <guid isPermaLink="true">https://thisisprogrammingworld.tistory.com/255</guid>
      <comments>https://thisisprogrammingworld.tistory.com/255#entry255comment</comments>
      <pubDate>Sun, 20 Jul 2025 15:33:40 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot에서 비동기 처리 구현 (@Async, @EnableAsync)</title>
      <link>https://thisisprogrammingworld.tistory.com/253</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;비동기적인 방식은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A가 B에게 작업을 요청하고, B의 작업이 실행되는 것과 상관없이 A 자신의 작업을 이어서 진행하는 방식을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기/비동기에 대한 구체적인 설명은 아래 글에 정리하였었다.&lt;/p&gt;
&lt;figure id=&quot;og_1750503312083&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;동기/비동기(Sync/Async), Blocking/Non-Blocking&quot; data-og-description=&quot;시작하기에 앞서, 간단히 기준이 되는 용어를 보겠습니다.Blocking/Non-Blocking : '제어권'을 두고 구분동기/비동기 : 결과값을 기다리느냐로 구분 1. Blocking VS Non-Blocking : 제어의 관점요청한 작업이 전&quot; data-og-host=&quot;thisisprogrammingworld.tistory.com&quot; data-og-source-url=&quot;https://thisisprogrammingworld.tistory.com/127&quot; data-og-url=&quot;https://thisisprogrammingworld.tistory.com/127&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LiaI9/hyZckD1ftr/kwKv6VUycWjqRLKoL8KaC0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bJzNg5/hyZck46fzv/MtKzJQKsnAZhQjfaBmS2UK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://thisisprogrammingworld.tistory.com/127&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://thisisprogrammingworld.tistory.com/127&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LiaI9/hyZckD1ftr/kwKv6VUycWjqRLKoL8KaC0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bJzNg5/hyZck46fzv/MtKzJQKsnAZhQjfaBmS2UK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;동기/비동기(Sync/Async), Blocking/Non-Blocking&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;시작하기에 앞서, 간단히 기준이 되는 용어를 보겠습니다.Blocking/Non-Blocking : '제어권'을 두고 구분동기/비동기 : 결과값을 기다리느냐로 구분 1. Blocking VS Non-Blocking : 제어의 관점요청한 작업이 전&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;thisisprogrammingworld.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;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서는 몇개의 애노테이션 설정만으로 비동기 방식을 구현할 수 있도록 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- @EnableAsync, @Async 이용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 설정 클래스에 @EnableAsync 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration 애노테션이 붙은 설정 클래스에 적용해주어야 비동기 기능이 활성화된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 해당 클래스 내에 비동기 작업을 처리하는 스레드 풀에 대한 설정도 함께 해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1751186725477&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableAsync
@Configuration
public class AsyncConfig {
    /*
    비동기 작업들 실행 시 사용될 Thread Pool
    모두 Default 값으로 설정
     */
    @Bean
    public Executor asyncThreadPoolExecutor() {
        ThreadPoolTaskExecutor executer = new ThreadPoolTaskExecutor();
        executer.setCorePoolSize(1);
        executer.setMaxPoolSize(Integer.MAX_VALUE);
        executer.setQueueCapacity(Integer.MAX_VALUE);
        executer.setThreadNamePrefix(&quot;AsyncThread-&quot;);
        executer.initialize();
        return executer;
    }
}&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;일반적으로, Spring 은 SimpleAsyncTaskExecutor 클래스를 사용해 비동기 처리를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 아래 두가지 방법으로 오버라이딩할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;애플리케이션 레벨에서 오버라이딩 : AsyncConfigurer 인터페이스를 implements하면서 getAsyncExecutor() 메서드를 오버라이딩하고, 이 메서드 내에서 원하는 구현체를 반환하도록 한다.&lt;/li&gt;
&lt;li&gt;개별 메서드 레벨에서 오버라이딩 : @Async 애노테이션의 name 속성을 이용하여 특정 Bean 이름을 지정할 수 있다. 이 애노테이션이 붙은 메서드에서는 지정된 클래스에서 관리되는 스레드 풀을 이용하게 된다.&amp;nbsp; ex)&amp;nbsp; &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; color: #63b175;&quot;&gt;@Async(&quot;threadPoolTaskExecutor&quot;)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadPoolTaskExecutor 는 일반 요청 시에는 사용되지 않는 스레드 풀이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 메서드를 수행할 때 (@Async를 적용하여 비동기 작업을 수행하려할 때, @EventListener를 적용한 비동기 이벤트 리스너가 수행될 때), 혹은 직접 이 Executor를 사용하겠다고 정의한 경우 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring Boot 3.x 에서의 ThreadPoolTaskExecutor 설정 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 3점대 를 사용한다면, ThreadPoolTaskExecutor 를 직접 설정하지 않고 설정 파일에 입력하는 것만으로도 설정 가능하며, Spring Boot가 자동으로 관리해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 여러 스레드 풀의 설정을 해줘야한다면 별도 설정 클래스에서 빈을 등록해주는 것이 관리 측면에서 좋을 것으로 보인다.&lt;/p&gt;
&lt;pre id=&quot;code_1750509814866&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml
spring:
  task:
    execution:
  pool:
    core-size: 5
    max-size: 10
    queue-capacity: 100
    thread-name-prefix: &quot;Async-&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadPoolTaskExecutor 클래스 에 대해 설정할 수 있는 값들은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;spring.task.execution.pool.core-size: core thread 갯수. 가상 스레드가 활성화되어 있으면 의미 없음. (Default : 8)&lt;/li&gt;
&lt;li&gt;spring.task.execution.pool.max-size: 늘릴 수 있는 최대 스레드 갯수. 큐가 가득 차버리면 (즉, 100개의 작업이 대기 중인 상황) 스레드 풀은 최대 16개까지 증가하도록 하여 부하를 수용할 수 있다. 다만 큐 용량이 무한대라면 무시되는 값이다. (Default : Integer 범위 최대 값)&lt;/li&gt;
&lt;li&gt;spring.task.execution.pool.queue-capacity : 큐 용량. 이 용량이 무제한이라면, max-size 속성이 무시된다. (Default : Integer 범위 최대 값)&lt;/li&gt;
&lt;li&gt;spring.task.execution.pool.keep-alive : 스레드가 회수되기 전 유휴 상태로 유지하는 시간 제한. 10초 설정하면, 10초 동안 유휴 상태가 유지되면 회수된다. (Default : 60s)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 비동기로 수행시키고 싶은 메서드에 @Async 적용&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략히 0부터 9까지 순회 출력하는 메서드를 작성하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 메서드는 public 접근제어자 로 구현되어야 한다.
&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: #5f6d2b;&quot;&gt;Spring에서는 AOP 기반의 프록시 패턴을 이용하여 프록시 객체를 통해 비동기 메서드에 접근한다.&amp;nbsp;&lt;br /&gt;프록시 객체를 거치지 않고 직접 메서드를 호출할 경우 비동기적으로 동작하지 않는다.&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #5f6d2b;&quot;&gt;물론 동일한 클래스 내부에서 해당 메서드를 직접 호출할 경우에도 비동기적으로 동작하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;같은 맥락으로, 자기 호출 (self-invocation) 하는 경우에도 비동기적으로 동작하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1751186197154&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class AsyncTask {

    @Async(&quot;asyncThreadPoolExecutor&quot;)
    public void async() {
        for (int i=0; i&amp;lt;10; i++) {
            log.info(&quot;async() - {}&quot;, i);
        }
        log.info(&quot;2. async() Method Finished. (time: {})&quot;, LocalTime.now());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 메서드를 호출하는 테스트코드를 작성하여,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드에서 async() 메서드를 호출 했을 때 async() 메서드 호출이 완료되기 전에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드 내 다음 로직이 실행되는지 확인하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1751186238778&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
class AsyncTaskTest {
    @Autowired
    private AsyncTask asyncTask;

    @Test
    void async() {
        System.out.println(&quot;1. Before async() Calling : &quot; + LocalTime.now());
        asyncTask.async();
        System.out.println(&quot;3. async() finished : &quot; + LocalTime.now());
    }
}&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;동기적으로 처리된다면 1 &amp;gt; 2 &amp;gt; 3. 순으로 메세지가 출력되지만,&amp;nbsp;비동기로 처리되어 1 &amp;gt; 3 &amp;gt; 2. 순으로 메세지가 출력되는 것을 확인하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-29 오후 6.06.01.png&quot; data-origin-width=&quot;2814&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCHxSw/btsOWkWfqdf/k1cNHuTOht95MNc9UBWjr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCHxSw/btsOWkWfqdf/k1cNHuTOht95MNc9UBWjr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCHxSw/btsOWkWfqdf/k1cNHuTOht95MNc9UBWjr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCHxSw%2FbtsOWkWfqdf%2Fk1cNHuTOht95MNc9UBWjr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2814&quot; height=&quot;1022&quot; data-filename=&quot;스크린샷 2025-06-29 오후 6.06.01.png&quot; data-origin-width=&quot;2814&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/reference/features/task-execution-and-scheduling.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-boot/reference/features/task-execution-and-scheduling.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-async&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.baeldung.com/spring-async&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✍  &lt;a href=&quot;https://github.com/BananMoon/playground/pull/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;관련 Github Code&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드 개발하며 작성한/Spring</category>
      <category>@Async</category>
      <category>@EnableAsync</category>
      <category>async</category>
      <category>Sync</category>
      <author>뭉지(moonz)</author>
      <guid isPermaLink="true">https://thisisprogrammingworld.tistory.com/253</guid>
      <comments>https://thisisprogrammingworld.tistory.com/253#entry253comment</comments>
      <pubDate>Sun, 29 Jun 2025 18:11:20 +0900</pubDate>
    </item>
    <item>
      <title>[AWS S3] Presigned URL과 생성 방식</title>
      <link>https://thisisprogrammingworld.tistory.com/251</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 개발하고 있는 서비스의 파일 저장소로써 AWS S3 서비스를 이용하려 하고 있다.&lt;br /&gt;어드민 사이트에서 파일을 업로드하면, 대외 서비스에서 특정 고객들에게만 해당 파일 다운로드를 제공하는 서비스인데,&lt;br /&gt;제한된 사용자에게만 파일을 제공하는 방법에 대해 고민하다 AWS 에서 제공하는 Presigned URL이란 방법을 알게 되었다.&lt;br /&gt;Presigned URL을 서비스에 적용하기까지 알아본 내용들을 글로 정리해두려 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Presigned URL&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presigned URL은 AWS S3에서 요청자의 권한을 제어하는데 제공하는 방법 중 하나이다.&lt;br /&gt;Presigned URL은 접근 제어 정책을 따로 업데이트하지 않고도 URL을 통해 임시적으로 접근 권한을 제공하는 것이다. 사용자에게 접근권한을 부여하는 토큰인 셈이다.&lt;br /&gt;S3에서 권한을 제어하는 방법들은 Presigned URL 외에도 IAM 사용자 정책, 액세스 포인트 등을 활용하는 방법들이 있다. 이 권한 제어 방법들은 별도 글에서 정리해보려 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presigned URL은 아래와 같이 생겼다.&lt;br /&gt;https://&amp;lt;버킷명&amp;gt;.s3.&amp;lt;리전명&amp;gt;.amazonaws.com/&amp;lt;객체명&amp;gt;?response-content-disposition=inline&amp;amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;amp;X-Amz-Security-Token=&amp;lt;임시 인증 정보&amp;gt;&amp;amp;X-Amz-Algorithm=&amp;lt;서명 방식&amp;gt;&amp;amp;X-Amz-Credential=&amp;lt;AWS 인증 정보&amp;gt;&amp;amp;X-Amz-Date=20250506T043725Z&amp;amp;X-Amz-Expires=60&amp;amp;X-Amz-SignedHeaders=host&amp;amp;X-Amz-Signature=&amp;lt;서명 값&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presigned URL을 생성할 때에는 아래 정보들이 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버킷 명&lt;/li&gt;
&lt;li&gt;객체 key (파일 경로와 파일 명)&lt;/li&gt;
&lt;li&gt;요청하려는 HTTP 메서드 (다운로드는 GET, 업로드는 PUT, 객체의 메타 데이터 읽는 것은 HEAD)&lt;/li&gt;
&lt;li&gt;URL 만료 시간
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS CLI 나 SDK로 URL을 생성한다면, 최대 7일까지 유효 기간을 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;만약 Presigned URL의 생성자의 권한이 수정/삭제/비활성화 처리 되면, 생성자의 Credentials로 만든 Presigned URL도 만료 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 Presigned URL을 받은 사용자는 제한 없이 Presigned URL을 이용해 특정 Operation을 할 수 있지만, URL 을 생성하려는 유저가 갖고 있는 Operation에 제한된다.&lt;br /&gt;예를 들어, 버킷 A에 대해 파일 읽기 권한만 있는 생성자는 버킷 A에 대한 파일 읽기 작업을 수행하는 Presigned URL만 생성할 수 있고, 쓰기 작업을 수행하는 Presigned URL은 생성할 수 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법 1) SDK를 이용한 Presigned URL 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 AWS SDK를 이용해 Presigned URL을 생성하는 방법이다.&lt;br /&gt;이 방법은 자격 증명을 거치고 나면 &lt;span style=&quot;color: #333333;&quot;&gt;따로 AWS S3 와 통신하지 않고도&amp;nbsp;&lt;/span&gt;애플리케이션에서 Presigned URL을 생성할 수 있는 방법이다.&lt;br /&gt;네트워크 통신을 하지 않아도 된다는 점이 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소스코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Import (gradle) :&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;    implementation(platform(&quot;software.amazon.awssdk:bom:2.27.21&quot;))
    implementation(&quot;software.amazon.awssdk:s3&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;AWS 자격 증명 값 세팅 방법 3가지 :&amp;nbsp;&lt;/b&gt;&lt;br /&gt;만약 접근하려는 버킷/파일의 접근 제어 정책이 세팅되어 있다면&lt;br /&gt;Presigned URL을 생성하는 역할의 객체를 Bean으로 등록할 때 인증/인가를 체크하기 위해 &lt;br /&gt;해당 버킷/파일의 Operation 권한이 있는 계정의 Credentials 정보를 세팅해야한다.&lt;br /&gt;AWS Credentials 정보를 세팅하는 방법은 3가지가 있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;기본 AWS 자격 증명 파일 (&lt;code&gt;루트/.aws/credentials&lt;/code&gt;)&lt;/b&gt;에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;설정된 AWS 자격 증명 값을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&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;&amp;lt;루트 경로&amp;gt;\.aws\credentials 파일에 아래 정보를 넣어준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[default]
aws_access_key_id = &amp;lt;버킷의-access-key&amp;gt;
aws_secret_access_key = &amp;lt;버킷의-secret-key&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소스코드에서는 ProfileCredentialsProvider&amp;nbsp;객체를 생성하면서 위 credentials 파일에 작성한 프로파일 값 ('default') 을 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AwsS3Config {
	@Bean(name = &quot;s3Presigner&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public S3Presigner s3PresignerWithCredentialFiles() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return S3Presigner.builder()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.region(Region.of(region))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.credentialsProvider(ProfileCredentialsProvider.create(&quot;default&quot;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.build();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;2. &lt;b&gt;설정 파일 (application.yaml 혹은 application.properties)&lt;/b&gt;에 설정된 AWS 자격 증명 값을 사용하여 인증하는 방법&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;&quot;&gt;설정 파일에&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;Access Key와 Secret Key를 직접 노출하여 세팅하는 것보다는 환경변수로 대체하는 것이 보안 상 안전하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Configuration
public class AwsS3Config {
	@Value(&quot;${amazon.s3.access-key}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String accessKey;
	@Value(&quot;${amazon.s3.secret-key}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String secretKey;
	@Value(&quot;${amazon.s3.region}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String region;		// ex: &quot;ap-northeast-2&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Bean(name = &quot;s3Presigner&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public S3Presigner s3PresignerWithStatic() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;AwsBasicCredentials awsCredentials = AwsBasicCredentials.create(accessKey, secretKey);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return S3Presigner.builder()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.region(Region.of(region))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.build();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;3. &lt;b&gt;운영 체제의 환경 변수&lt;/b&gt;에 설정된 AWS 자격 증명 값을 사용하여 인증하는 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자격증명 정보를 세팅한 AWSStaticCredentialsProvider 객체를 AmazonS3ClientBuilder에 전달하여 빌드한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configurations
public class AmazonConfig {
	@Value(&quot;${amazon.s3.access-key}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String accessKey;
	@Value(&quot;${amazon.s3.secret-key}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String secretKey;
	@Value(&quot;${amazon.s3.region}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String region;		// ex: &quot;ap-northeast-2&quot;

	@Bean
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public AmazonS3 awsS3Client() {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;BasicAWSCredentials basicAWSCredentials = AwsBasicCredentials.create(accessKey, secretKey);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ClientConfiguration clientConfiguration = new ClientConfiguration();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clientConfiguration.setProtocol(Protocol.HTTPS);
 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return S3Client.builder()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.region(region)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.credentialsProvider(StaticCredentialsProvider.create(basicAWSCredentials))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.clientConfiguration(clientConfiguration)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.build();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;만약 Access Key가 잘못되거나 Secret Key가 잘못되어 세팅되면 403 상태 코드와 함께 아래와 같은 에러 코드를 반환받는다.&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;InvalidAccessKeyId : Access Key를 잘못 세팅한 경우&lt;/li&gt;
&lt;li&gt;SignatureDoesNotMatch : Secret Key를 잘못 세팅한 경우&lt;/li&gt;
&lt;li&gt;Access Denied : 권한이 없는 경우 (Presigned URL이 만료된 경우 등 여러 권한에 문제되는 상황에서 반환된다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;SDK 통한 생성 메서드 (java) :&lt;/b&gt;&lt;br /&gt;GET 요청을 할 수 있는 Presigned URL을 생성하는 로직이다.&amp;nbsp;S3Presigner&amp;nbsp;객체를 이용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급했던 버킷명, 객체명, 유효 시간, 요청 메서드 를 입력한다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import software.amazon.awssdk.utils.IoUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Duration;

@Slf4j
@Service
@RequiredArgsConstructor
public class AmazonS3Utils {
    private final S3Presigner s3Presigner;

    /* Create a pre-signed URL to download an object in a subsequent GET request. */
    public String createPresignedGetUrl(String bucketName, String keyName) {
        GetObjectRequest objectRequest = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(keyName)
                .build();
        GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
                .signatureDuration(Duration.ofMinutes(10))  // The URL will expire in 10 minutes.
                .getObjectRequest(objectRequest)
                .build();

        PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest);

        log.info(&quot;Presigned URL: [{}]&quot;, presignedRequest.url().toString());
        log.info(&quot;HTTP method: [{}]&quot;, presignedRequest.httpRequest().method());

        return presignedRequest.url().toExternalForm();

    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법 2) AWS Lambda를 이용한 Presigned URL 생성 (feat. python - boto3 라이브러리)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presigned URL 생성 로직을 애플리케이션에서 하지 않고 AWS 에서 제공하는 Lambda 를 이용할 수 있다.&lt;br /&gt;AWS Lambda는 서버를 프로비저닝하고 관리하는 것 없이 코드를 실행시킬 수 있도록 제공하는 AWS 서비스이다. (지원하는 언어는 Node.js, Python, Ruby, Typescript, Java, Go, C# 이다.)&lt;br /&gt;Lambda는 필요할 때 함수를 실행시키고, 트래픽에 따라 자동 확장을 지원하기 때문에&lt;br /&gt;실시간 처리가 필요한 기능에서 유용하게 사용된다.&lt;br /&gt;우리 팀에서 Presigned URL 생성 로직 실행을 애플리케이션이 아닌 Lambda 서비스를 이용하게 된데에는 아래 세가지 정도의 이점이 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;중앙 집중화&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 생성하는 S3 버킷은 여러 유관 부서 혹은 서비스에서도 호출할 가능성이 있다.&lt;br /&gt;그래서 사용자의 권한 관리를 쉽게 하는 것 등 앞으로의 운영 편의성도 고려해야 했다.&lt;br /&gt;별개로 Lambda 함수를 실행시키기 전, 호출 서비스들에 대한 검증 절차도 필요했는데&lt;br /&gt;AWS의 API Gateway 를 이용하여 API Gateway에서 해당 요청을 검증하는 Authorizer 를 등록해 이를 수행하도록 했다.&lt;br /&gt;Authorizer에는 다양한 인증 처리 방식을 사용할 수 있는데,&lt;br /&gt;이번에 만드는 버킷의 경우, 요청 서비스들이 공통적으로 사용자 토큰 (jwt 토큰)을 가지고 있었기 때문에 JWT 토큰을 검증하는 로직을 Authorizer로 등록하여 요청 클라이언트를 인증하는 방식도 통일하였다.&lt;br /&gt;Authorizer도 Lambda 로 구현하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 나의 상황과 달리&lt;br /&gt;호출하는 서비스마다 인증하는 방식이 상이하다면, 인증 절차를 위한 Authorizer Lambda 함수를 서비스 갯수만큼 추가해야할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2. 보안 강화 (보안 중앙 관리)&lt;/b&gt;&lt;br /&gt;S3 버킷에 접근해야하는 여러 사용자(부서/서비스) 별 IAM 정책을 생성해주어야 하는데,&lt;br /&gt;S3 버킷의 Presigned URL을 생성하는 주체를 Lambda로 제한하게 되면 각 사용자 (부서/서비스)마다 IAM 권한을 부여/관리 하지 않아도 된다.&lt;br /&gt;그리고 이를 사용하고자 하는 서비스들은 해당 Lambda를 호출할 수 있는 JWT 토큰만 갖고 있으면 된다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;3. 유지보수 용이&lt;/b&gt;: Presigned URL 생성 로직을 한곳에서 관리하면,&lt;br /&gt;여러 서비스에서 중복으로 로직을 개발해야 하는 부담을 줄일 수 있고&lt;br /&gt;Presigned URL 생성 로직을 수정하거나 인증 방식을 수정해야할 때 Lambda 함수에서만 수정하면 된다는 점에서 유지보수가 용이해진다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;4. 로깅 용이&lt;/b&gt; : Presigned URL을 생성하며 발생하는 이슈에 대해 모니터링 시에는 CloudWatch Logs로 중앙 관리/확인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lambda 함수 코드 내에서 출력하는 로그는 자동으로 CloudWatch Logs에 전송된다.&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;결론적으로, 호출자 검증 및 권한 관리를 위한 Authorizer 함수와 S3 Presigned URL 생성 함수를 Lambda에 등록하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 Lambda 서비스를 사용할 때 고려해야할 점들도 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lambda 서비스 사용 시 고려해야할 단점.&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Cold Start&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda 서비스는 외부 트리거에 의해 Lambda 인스턴스가 함수를 실행하는 방식으로 동작하는데,&lt;br /&gt;만약 인스턴스가 없거나, 기존 인스턴스가 만료되었다면, 함수 인스턴스를 생성하면서 컨테이너 준비, 초기화 작업을 거치는 과정을 수행해야 한다.&lt;br /&gt;이로 인해 사용자 응답에 지연이 발생하게 된다.&lt;br /&gt;특히 요청이 갑자기 몰렸을 때에는 이를 처리하기 위해 신규 인스턴스들을 생성하고 이때에도 초기화 작업을 수행하게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;2. 제한된 환경 (실행 시간, 메모리 사용량, 환경 변수 크기)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda 로 함수를 실행하는 것에는 몇가지 제한이 있다.&lt;br /&gt;실행 시간은 최대 900초로 제한되고, 메모리도 본인이 설정한 값으로 제한되기 때문에 함수가 실행되는 동안의 메모리 사용량도 확인해야 한다.&lt;br /&gt;Lambda는 환경 변수를 사용하여 함수에 설정 정보를 제공하는데, 제한 크기가 4KB이다.&lt;br /&gt;그래서 Lambda 로 실행시키고자 하는 로직이 실행 시간이 너무 소요되거나, 많은 메모리를 필요로 한다면 Lambda를 사용하기 어렵다는 문제가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;나의 선택은,&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 우리가 방법 2를 선택한데에는 아래와 같은 배경도 고려하였기 때문에, 운영하는 서비스와 팀의 상황에 맞게 다른 선택을 할 수 있을 것이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Lambda 서비스를 사용하면 결국 네트워크도 타고, 비용도 발생하는 것이기 때문에 개인적으로는 방법 1) 이 낫지 않을까 란 생각이었는데&lt;br /&gt;현재 요청 트래픽 수준에서는 비용이 감당 가능하며,&lt;br /&gt;여러 서비스에서 중복 구현해야하는 이슈를 해결하는 것에 더 의의를 두고자 한다는 DevOps의 의견에 따르기로 했다.&lt;/li&gt;
&lt;li&gt;팀 내 DevOps에서 S3 서비스를 관리하는데, 여러 서비스가 호출할 버킷이다보니 관리/운영을 쉽게하고자 하는 니즈도 있었다.&lt;/li&gt;
&lt;li&gt;제공할 서비스가 Cold Start 이슈를 고려할만큼 사용자 트래픽이 한번에 다량으로 발생하지는 않는다는 점도 이유이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가) 서비스 구성 : API Gateway + Lambda&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS의 API Gateway 서비스는 REST 및 WebSocket API를 생성, 게시, 유지, 모니터링 및 보호하기 위한 AWS 서비스이다.&lt;br /&gt;우리 팀에서는 AWS API Gateway를 구성하여, AWS Lambda 서비스를 호출하는 요청을 검증하는 Authorizer를 추가하였다.&lt;br /&gt;이외에도 기본적으로 설정된 공통 응답을 제공하고&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(&amp;lsquo;게이트웨이 응답&amp;rsquo; 메뉴에서 확인 가능하다.)&lt;/span&gt;&amp;nbsp;사용량을 제어할 수 있는 등 기본적으로 게이트웨이에서 수행하는 기능들을 제공한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;lt;참고 이미지: API Gateway 를 Lambda 서비스의 트리거로써 연동&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XPD8K/btsNKghgEz0/9PI9E246nxX6E3sNdP9hrk/img.png&quot; width=&quot;552&quot; height=&quot;259&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloud Front까지 구성한다고 하면 아래와 같은 모습이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 구성도이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfxDu5/btsN2MAfSuB/53cu85MZRzpaWcw1i7B161/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfxDu5/btsN2MAfSuB/53cu85MZRzpaWcw1i7B161/img.png&quot; data-alt=&quot;인증 없이 접근 가능한 버킷과 인증을 통해서만 접근 가능한 버킷을 자물쇠로 구분하였다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfxDu5/btsN2MAfSuB/53cu85MZRzpaWcw1i7B161/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfxDu5%2FbtsN2MAfSuB%2F53cu85MZRzpaWcw1i7B161%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;645&quot; height=&quot;385&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인증 없이 접근 가능한 버킷과 인증을 통해서만 접근 가능한 버킷을 자물쇠로 구분하였다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>클라우드</category>
      <category>AWS API gateway</category>
      <category>AWS CloudFront</category>
      <category>AWS Lambda</category>
      <category>aws s3</category>
      <category>presigned url</category>
      <author>뭉지(moonz)</author>
      <guid isPermaLink="true">https://thisisprogrammingworld.tistory.com/251</guid>
      <comments>https://thisisprogrammingworld.tistory.com/251#entry251comment</comments>
      <pubDate>Tue, 6 May 2025 13:45:22 +0900</pubDate>
    </item>
    <item>
      <title>[Nginx] 파일 접근 시 Permission denied 오류 해결</title>
      <link>https://thisisprogrammingworld.tistory.com/250</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;nginx를 설치하고 실행해보는 작업을 진행하는 중에 발생한 오류이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 내 특정 경로에 있는 이미지를 가져올 수 있도록 nginx에서 루트 경로 설정을 한 뒤 브라우저에서 해당 이미지를 조회하니, 아래와 같이 403 에러가 발생하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmfu1G/btsMwqdvyn4/lzOQyE7B4IeSOuKk6jwTKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmfu1G/btsMwqdvyn4/lzOQyE7B4IeSOuKk6jwTKK/img.png&quot; data-alt=&quot;파일명도 잘못 조회한거였지만, 미리 준비해놓은 에러 화면이 아닌 403 화면이 나온 것이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmfu1G/btsMwqdvyn4/lzOQyE7B4IeSOuKk6jwTKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdmfu1G%2FbtsMwqdvyn4%2FlzOQyE7B4IeSOuKk6jwTKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1728&quot; height=&quot;159&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파일명도 잘못 조회한거였지만, 미리 준비해놓은 에러 화면이 아닌 403 화면이 나온 것이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✍  접근 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx의 error.log 파일을 확인해보니 아래와 같이 Permission Denied 가 출력되어있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #3366ff;&quot;&gt;[error] 7781#0: *18 open() &quot;/nas/error.html&quot; failed (13: Permission denied), client: {접근 IP}, server:&lt;span&gt; {접근 Host}&lt;/span&gt;, request: &quot;GET /favicon.ico HTTP/1.1&quot;, host: &quot;&lt;span style=&quot;background-color: #ffffff; color: #3366ff; text-align: start;&quot;&gt;{서버 IP}&lt;/span&gt;&quot;, referrer: &quot;&lt;a style=&quot;color: #3366ff;&quot; href=&quot;https://172.21.30.8/image/htdocs/img_upload/product/2306223907131079.png&quot;&gt;https://&lt;span style=&quot;background-color: #ffffff; color: #3366ff; text-align: start;&quot;&gt;{서버 IP}&lt;/span&gt;/3907131079.png&lt;/a&gt;&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그에서는 /nas/error.html 파일을 열려고 했는데, Permission denied 이슈로 이 파일을 여는데 실패했다고 말하고 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 요청한&amp;nbsp;&lt;a style=&quot;color: #3366ff;&quot; href=&quot;https://172.21.30.8/image/htdocs/img_upload/product/2306223907131079.png&quot;&gt;3907131079.png&lt;/a&gt; 를 조회하려했는데, 해당 디렉토리에 파일이 없어서 nginx 설정 파일에서 설정한 에러 파일 error.html 을 로드하려고 그 에러 파일에 접근하니 시스템에서 권한 이슈로 거부를 한것으로 최종 파악하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Permission Denied 이니, 디렉토리 혹은 파일의 권한 문제로 유추하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;접근하려한 파일과 디렉토리를 확인하다보니 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;해당 파일의&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; 루트 디렉토리의 소유자가 root 임을 확인하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;그런데 Nginx는 user1 라는 계정으로 동작 중이었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;**Nginx 실행 유저는 nginx 설정 파일에서 확인할 수 있다. (파일명 : nginx.conf)&lt;/p&gt;
&lt;pre id=&quot;code_1740484556642&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ cat nginx.conf
user  user1;    # user1로 설정해둔 것 확인.
worker_processes  4;
...생략&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-macro-name=&quot;code&quot; data-hasbody=&quot;true&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;
&lt;div id=&quot;highlighter_162172&quot; style=&quot;background-color: #ffffff;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;디렉토리/파일의 소유자는 'll' 명령어만으로도 확인 가능하다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;⭐️ 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;루트 디렉토리의 소유자를 nginx가 실행되는 계정&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;으로 변경하였다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 파일 소유자에 대해 쓰기, 읽기, 실행할 수 있도록 권한을 설정하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1740484591406&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ chmod user1:user1 -R {루트 경로}
$ chown 755 -R {루트 경로}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-hasbody=&quot;true&quot; data-macro-name=&quot;code&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;highlighter_162172&quot; style=&quot;background-color: #ffffff;&quot;&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 존재하지 않는 경로를 호출해보니 error.html 파일이 정상 로드되는 것을 확인하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인&lt;/b&gt;은 nginx에서 루트로 설정한 디렉토리들의 권한은 root:root로 부여되어 있는데 nginx 실행 유저는 user1로 설정해두었기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 디렉토리 하위에 있는 파일에 접근하려하여 문제가 된 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;루트 디렉토리의 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;파일 소유자를 &lt;/span&gt;nginx 실행 계정과 동일하게 지정하고,&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파일 소유자에 대해 파일 쓰기/읽기/실행 권한을 부여하여 &lt;b&gt;해결&lt;/b&gt;하였다.&lt;/p&gt;</description>
      <category>TroubleShooting</category>
      <author>뭉지(moonz)</author>
      <guid isPermaLink="true">https://thisisprogrammingworld.tistory.com/250</guid>
      <comments>https://thisisprogrammingworld.tistory.com/250#entry250comment</comments>
      <pubDate>Tue, 25 Feb 2025 21:05:53 +0900</pubDate>
    </item>
    <item>
      <title>Cookie  (feat. JWT Token, SSO)</title>
      <link>https://thisisprogrammingworld.tistory.com/244</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzz0d/btsIQI28MyC/kcPGG5DU1bPTFl6gHwKe0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzz0d/btsIQI28MyC/kcPGG5DU1bPTFl6gHwKe0k/img.png&quot; data-alt=&quot;사용자 인증 토큰을 쿠키 스토리지에?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzz0d/btsIQI28MyC/kcPGG5DU1bPTFl6gHwKe0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkzz0d%2FbtsIQI28MyC%2FkcPGG5DU1bPTFl6gHwKe0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;541&quot; height=&quot;204&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;504&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;단일 서비스의 사용자 인증 기능을 위해 동작하던 &quot;사용자 인증 애플리케이션&quot;에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다수 서비스의 사용자 인증 기능을 제공하는 서버로 동작해야함과 동시에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 서버를 통해 인증된 사용자는 사내 다른 서비스에 접속시에도 로그인 상태를 공유해야했기 때문에 SSO 기능까지 가능하도록 구현해야했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 방식으로 구현할 지 고민한 내용 중 JWT 토큰을 저장할 위치와 관련하여 &quot;쿠키&quot; 에 대해 알아본 내용들을 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSO 기능을 제공하려면 크게 두가지 요구사항이 있었습니다.&lt;i&gt;&lt;br /&gt;&lt;/i&gt;1) 로그인 후, 새 탭에서 접속했을 때에도 로그인이 되어야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2) 서브 도메인이 같으면 인증 토큰이 공유되어야 한다. (단, 다른 서브 도메인에 접속 시에는 쿠키가 전달되지 않는다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &quot;사용자 인증 애플리케이션&quot;은 사용자 인증 과정을 거치고 나면 JWT를 발급해주고 있었습니다. body에 토큰을 담아 응답하면 Frontend에서 이를 세션 스토리지에 저장하고 있었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠시 세션 스토리지와 로컬 스토리지, 그리고 쿠키에 대해 알아보겠습니다.&lt;/p&gt;
&lt;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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Session Storage&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 key-value를 저장하는 스토리지입니다.&lt;/li&gt;
&lt;li&gt;말그대로 브라우저 세션 동안만 데이터가 유지됩니다.&lt;/li&gt;
&lt;li&gt;탭 간에도 공유되지 않기 때문에 &lt;span style=&quot;caret-color: auto; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;같은 브라우저라도 다른 탭을 띄우면 데이터는 공유되지 않습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;브라우저 또는 탭을 닫으면 데이터가 삭제됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활용&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;세션 스토리지는 브라우저 탭 내로 제한되기 때문에 잘 활용되지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;잠시 필요한 정보 (일회성 로그인) 같은 데이터는 저장할 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.62em; letter-spacing: -1px; caret-color: auto; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;Local Storage&lt;/span&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 key-value를 저장하는 스토리지입니다.&lt;/li&gt;
&lt;li&gt;세션 스토리지와 유사하지만, 오리진(&lt;span style=&quot;background-color: #ffffff; color: #313130; text-align: start;&quot;&gt;domain/port/protocol)&lt;/span&gt;이 같으면 (동일 출처 내의) 모든 탭에서 데이터가 공유됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호스트 :&amp;nbsp;&lt;a href=&quot;https://www.example.com&quot;&gt;https://www.example.com&lt;/a&gt;의 로컬 스토리지 데이터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://api.example.com&quot;&gt;https://api.example.com&lt;/a&gt;과 공유되지 않습니다.&lt;/li&gt;
&lt;li&gt;프로토콜 :&amp;nbsp;&lt;a href=&quot;http://example.com&quot;&gt;http://example.com&lt;/a&gt;에서 저장한 &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: left;&quot;&gt;로컬 스토리지 &lt;/span&gt;데이터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://example.com&quot;&gt;https://example.com&lt;/a&gt;과 공유되지 않습니다.&lt;/li&gt;
&lt;li&gt;포트 번호 :&amp;nbsp;&lt;a href=&quot;https://example.com:8080&quot;&gt;https://example.com:8080&lt;/a&gt;에서 저장한 &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: left;&quot;&gt;로컬 스토리지 &lt;/span&gt;데이터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://example.com:3000&quot;&gt;https://example.com:3000&lt;/a&gt;과 공유되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: left;&quot;&gt;브라우저를 종료하거나 OS를 재시작해도&amp;nbsp;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;데이터가 삭제되지 않습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지속적으로 필요한 데이터 를 저장하는데 활용됩니다.&lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 스토리지와 로컬 스토리지보다 먼저 나왔던 기능이 쿠키 입니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Cookie&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP Cookie는 세션 스토리지/쿠키 스토리지와 다르게&amp;nbsp;데이터를 저장할 때 만료 기한을 정할 수있습니다.&lt;/li&gt;
&lt;li&gt;웹사이트에 접근하는 디바이스에 Cookie가 저장되며, 여러 개의 Cookie가 저장될 수 있으나 4kb라는 용량 제한이 있습니다.&lt;/li&gt;
&lt;li&gt;Cookie는 브라우저 단위로 생성됩니다.&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;즉 Chrome, Explorer, Edge, Safari, FireFox, Opera 등 서로 다른&lt;/span&gt;&lt;span&gt; 브라우저에서 생성된 Cookie는 서로 다른 것으로 인식됩니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;요청을 할 때마다 서버로 저장된 &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;Cookie&lt;/span&gt;&lt;span style=&quot;caret-color: auto; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;도 함께 전달되기 때문에 그중 필요하지 않은 데이터들이 있다면 용량을 불필요하게 낭비하게 됩니다.&lt;br /&gt;&lt;/span&gt;서버로 전송되지 않아도 되는 데이터들은 로컬/세션 스토리지에 저 장하는 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활용&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HTTP 요청 자체만 보낼 때에는 서버가 누구인지 알 수 없습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;Cookie&lt;/span&gt;에 내가 누구인지 담아서 서버에 요청을 보낼 때 함께 전달하면 서버는 &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;Cookie&lt;/span&gt;를 읽어서 요청자가 누구인지 파악합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 Cookie는 웹 서버가 사용자의 기기에 'stateful'한 정보를 저장할 수 있도록 하기 때문에&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;온라인 쇼핑몰에서 고객의 장바구니에 아이템을 추가한 상태 정보를 저장하거나 '일주일간 다시보지 않기'와 같은 기능에 사용되는 등&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;사용자의 브라우저 활동을 트래킹&quot;&lt;/b&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;letter-spacing: 0px;&quot;&gt;그리고 제가 Cookie를 사용하고자 하는 목적이기도 한,&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;letter-spacing: 0px;&quot;&gt;웹서버에서 사용자가 로그인했는지, 그들이 어떤 계정으로 로그인했는지를 &quot;인증&quot;하는데에도 사용될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 인증을 거치고 나면 Cookie에 정보를 저장함으로써&amp;nbsp;사용자가 민감한 정보를 포함한 페이지 각각에&amp;nbsp;접근할 때 마다&lt;span&gt;&amp;nbsp;&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;하위 도메인 간 값이 공유되어야 하기 때문에, 이를 지원하는 쿠키를 선택했습니다. (&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;쿠키는 하위 도메인이 동일하면 공유가 가능합니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 새 탭으로 로그인했을 때 쿠키 스토리지 내 값은 사라지지 않는다는 특징이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;그래서 쿠키에 JWT 토큰을 저장하는 방식으로 시도해보기로 하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;다만 쿠키에 값을 저장하면 &lt;/span&gt;서버에 요청을 보낼 때 자동으로 값이 전송되기 때문에 보안 공격에 취약합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 쿠키에 값을 저장할 때 얼마나 값을 유지할지도 설정할 수 있는데요 이 기간도 길게 설정한다면 보안적으로는 더 취약하겠죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;그래서 쿠키를 적용하면서 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;최대한 보안에 취약하지 않는 방안이 있을지 찾아보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&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;color: #000000; font-size: 1.25em; letter-spacing: -1px;&quot;&gt;Cookie의 옵션 종류&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cookie에 값을 저장할 때는 다양한 종류의 옵션을 함께 지정할 수 있는데, 이 옵션들을 통해 브라우저에서 정보가 저장되고 공유될 수 있는 상태를 제한할 수 있습니다.&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;background-color: #99cefa;&quot;&gt;Max-Age&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cookie가 브라우저에 유 지되는 최대 설정 옵션 (단위 : second)&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: #172b4d; text-align: start;&quot;&gt;-1로 세팅 시 브라우저가 종료될 때까지 유지되는 Cookie가 생성된다. (일명 '&lt;span style=&quot;color: #172b4d; text-align: start;&quot;&gt;세션 쿠키')&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;Expires&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #172b4d; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;'Session' 이라는 값으로 세팅&lt;/span&gt; 시 브라우저 종료될 때까지 유지되는 Cookie가 생성된다. (일명 '세션 쿠키')&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Session cookie 와 Persistent cookie&lt;/b&gt;&lt;br /&gt;1. Session Cookie&lt;br /&gt;인메모리 쿠키라고도 알려져있는 세션 쿠키는 메모리에 저장됩니다.&lt;br /&gt;사용자가 웹사이트를 돌아다닐 동안 즉,&amp;nbsp;&lt;b&gt;브라우저 세션 동안만&amp;nbsp;일시적으로 존재&lt;/b&gt;합니다.&lt;br /&gt;결국 웹 브라우저를 닫으면 자동으로 삭제되는 것이 가장 큰 특징입니다.&lt;br /&gt;그렇기 때문에 아래에 나올 Persistent cookie (지속성이 있는 쿠키) 보다 보안 측면에서 유리합니다.&lt;br /&gt;반면 Persistent cookie는 말 그대로, 브라우저에 오랜 기간 저장되기 때문에 그만큼 공격에 노출될 가능성이 높습니다.&lt;br /&gt;&lt;br /&gt;세션 쿠키로 세팅하기 위해서는 쿠키의 유효 시간을 지정하는 쿠키의 옵션인 Expires 옵션을 'Session'으로 세팅하거나 Max-Age 옵션을 -1로 사용하면 됩니다.&lt;br /&gt;사용자가 브라우저를 닫을 때 무조건 쿠키에 저장한 정보가 초기화되어야 한다면 세션 쿠키를 사용하면 된다.&lt;br /&gt;&lt;br /&gt;2. Persistent Cookie&lt;br /&gt;지속성이 있는 쿠키는 특정 일자나 기간에 만료되는 쿠키입니다.&amp;nbsp;&lt;br /&gt;Persistent cookie는 디스크에 저장되기 때문에, 브라우저를 닫거나 컴퓨터를 재시작하더라도 남아있게 된다.&lt;br /&gt;이러한 기능 덕분에, &lt;br /&gt;사용자가 브라우저를 닫고 다시 재접속했을 때에도 이전에 등록했던 정보들을 저장하고 있어 사용자가 재접속했을 때 로그인을 유지시켜주는데 쓰이기도 합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;앞서 말한 대로, Persistent cookie는 Session cookie보다 공격에 노출될 가능성이 높기 때문에 보안에 도움이 되는 여러 옵션들과 함께 생성하길 권장하고 있습니다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;Secure&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 옵션이 설정되면, 쿠키가 암호화된 연결 간에만 전달될 수 있도록 하고 암호화되지 않은 연결 간에는 전달되지 못하도록 합니다. HTTPS가 대표적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해, 네트워크 도청 (스니핑/스누핑 공격) 으로 인해 쿠키가 도난당할 가능성을 줄여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;Http-only&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 옵션이 설정되면, Javascript와 같은 클라이언트 사이드에서 접근할 수 없게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 제한은 XSS 공격으로부터 막아주는 효과가 있습니다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;XSS 공격 &amp;nbsp;(Cross Site Scripting, XSS) 이란?&lt;/b&gt;&lt;br /&gt;공격자가 웹 페이지에 악의적인 스크립트를 삽입하여, 사용자의 브라우저에서 실행되게 만드는 공격입니다.&lt;br /&gt;이를 통해 공격자는 사용자의 세션 쿠키를 탈취하거나, 사용자를 가장하여 악의적인 행위를 수행할 수 있습니다.&lt;br /&gt;&lt;br /&gt;기본적으로 Cookie는 Javascript를 통해 조회 가능한데, 쿠키를 HttpOnly 설정하면 이를 막을 수 있는 효과가 있습니다.&amp;nbsp;&lt;/blockquote&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;HttpOnly 옵션을 설정하면 서버에서만 쿠키를 읽을 수 있기 때문에&lt;/b&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;한번 이상의 서버로의 API 호출이 불가피합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 클라이언트 측에서 쿠키 값을 확인하거나 수정하는 작업이 불가능합니다. 이러한 단점을 감수하더라도 보안을 위해 HttpOnly 옵션을 사용하는 것이 안전합니다.&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;background-color: #99cefa;&quot;&gt;Same-site&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Same-site 옵션은 쿠키를 전달할 수 있는 정도를 제한하는 옵션입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태는 총 세가지로 설정할 수 있습니다. Strict / Lax / None&lt;/p&gt;
&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;- Strict&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 하위 도메인이 같은 도메인으로만 쿠키를 전달할 수 있도록 제한합니다. 즉 퍼스트(first) 쿠키만이 전달됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;blog&lt;u&gt;.naver.com&lt;/u&gt; 과 map&lt;u&gt;.naver.com&lt;/u&gt; 은 하위도메인이 &lt;u&gt;.naver.com&lt;/u&gt; 으로 같기 때문에 쿠키를 공유할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 CSRF 공격에 방지하는데 도움이 됩니다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;CSRF 공격이란?&lt;/b&gt;&lt;br /&gt;CSRF 공격은 사용자가 로그인한 상태에서 공격자가 준비한 악의적인 요청을 서버에 전송하게 만듭니다. 이 과정에서 사용자는 자신도 모르는 사이에 공격자가 의도한 행위를 실행하게 됩니다.&lt;br /&gt;SameSite 쿠키 속성을 설정하면, 하위 도메인이 동일한 곳으로만 쿠키를 전달하도록 제한하여 쿠키가 의도치 않은 곳으로 전송되는 것을 막을 수 있습니다.&lt;/blockquote&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;- Lax&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 하위 도메인이 같은 도메인으로 쿠키를 전달할 수 있을 뿐만 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 도메인이 다른 도메인에도 쿠키를 전달할 수 있지만, 이때는 서버의 상태를 바꾸지 않을 것이라 기대하는 요청(ex. GET 메서드) 시에만 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 다른 출처의 요청인 third-party 쿠키 는 전달 불가합니다. 다만 예외적으로 &lt;span&gt;Top Level Navigation&amp;nbsp;&lt;/span&gt;(웹 페이지 이동) 의 경우 유저가 링크를 누르거나 리다이렉트를 통해 이동된 경우에는 third-party 쿠키가 전달됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;ㄴ&amp;nbsp;&lt;/span&gt;&amp;lt;img&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;태그나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;iframe&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;태그 내에서 페이지를 이동하는 경우에는 Top Level 이라 부를 수 없으므로 전송되지 않습니다.&lt;/p&gt;
&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;- None&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 하위 도메인이든, 다른 도메인이든 상관없이 쿠키가 전달 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;None으로 하려면 무조건 쿠키의 Secure 옵션을 true로 설정해야 정상 전송됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 만약 SameSite 속성을 None 으로 설정했음에도 Secure 속성이 false라면 쿠키가 전달되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ff0010;&quot;&gt;크롬 브라우저는 2020년 2월 배포된 80버전부터 same-site 옵션의 기본값을 Lax로&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 id=&quot;id-쿠키..그리고..인증-추가)HSTS(HTTPStrictTransportSecurity)&quot; style=&quot;color: #292929; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #003366;&quot;&gt;HSTS (HTTP Strict Transport Security)&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안에 취약하지 않게 하는 또다른 방법으로, HTTPS를 강제하는 기술이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HSTS는&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt; 브라우저와 웹 서버 간 통신 시에 무조건 HTTPS 통신만 할 수 있도록 강제하는 기술입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;이용자가 웹 서버에 처음 접속할 때, 응답 헤더에 Strict-Transaport-Security 헤더를 내려보내서&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;HSTS를 지원하는 브라우저라면 이를 해석하고 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;추후 접속 부터는 해당 도메인에 대해 HTTPS 로만 접속합니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;만약 브라우저에서 HTTP 접속이 시도되면 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;HTTP 응답 코드 302를 이용해 페이지 이동(redirection)이 일어나면서 강제로 HTTPS로 프로토콜을 변경시킵니다.&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;HSTS 설정 시 지정할 수 있는 값이 3개 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 80px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 19.9612%; height: 20px;&quot;&gt;설정&lt;/td&gt;
&lt;td style=&quot;width: 20.3101%; height: 20px;&quot;&gt;값&lt;/td&gt;
&lt;td style=&quot;width: 59.7286%; height: 20px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 19.9612%; height: 20px;&quot;&gt;max-age&lt;/td&gt;
&lt;td style=&quot;width: 20.3101%; height: 20px;&quot;&gt;31536000 (s)&amp;nbsp; -&amp;gt; 1년&lt;/td&gt;
&lt;td style=&quot;width: 59.7286%; height: 20px;&quot;&gt;해당 시간동안 HSTS 응답을 받은 웹 사이트에 대해 HTTPS 접속을 강제함.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 19.9612%; height: 20px;&quot;&gt;includeSubDomains&lt;/td&gt;
&lt;td style=&quot;width: 20.3101%; height: 20px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;width: 59.7286%; height: 20px;&quot;&gt;HSTS가 적용될 도메인의 서브 도메인까지 적용되고&amp;nbsp; 있음을 알려줌.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 19.9612%; height: 20px;&quot;&gt;preload&lt;/td&gt;
&lt;td style=&quot;width: 20.3101%; height: 20px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;width: 59.7286%; height: 20px;&quot;&gt;브라우저의 Preload List에 도메인 추가할 것을 알려줌.&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;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Strict-Transport-Security:&amp;nbsp;max-age=31536000;&amp;nbsp;includeSubDomains&amp;nbsp;;&amp;nbsp;preload&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버를 예로 봐보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 호출 시 응답 헤더 내에 Non-Authoritative-Reason: HSTS 라는 세팅과 함께 응답을 받고 (사진)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 바로 https로 리다이렉트 됩니다. (초록색 Network)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1247&quot; data-origin-height=&quot;473&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JWTRV/btsIWwUWVae/hvWx67Z9qqxLHLcGRJsiD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JWTRV/btsIWwUWVae/hvWx67Z9qqxLHLcGRJsiD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JWTRV/btsIWwUWVae/hvWx67Z9qqxLHLcGRJsiD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJWTRV%2FbtsIWwUWVae%2FhvWx67Z9qqxLHLcGRJsiD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1247&quot; height=&quot;473&quot; data-origin-width=&quot;1247&quot; data-origin-height=&quot;473&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;h3 data-ke-size=&quot;size23&quot;&gt;HSTS Preload List&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HSTS Preload List는 HSTS가 적용된 웹 사이트의 명단 정보에 대한 리스트 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인의 서비스에 HTTPS 적용을 한 후 HSTS Preload List에 등록하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이용자가 서비스에 처음 접속하는 브라우저에서부터 HTTPS로 접속하게 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 등록하기 위해 준수해야하는 가이드가 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가입 후 SSL 보안인증서 만료 등과 같이 가이드를 지키지 못하는 이슈가 있을 때 사이트가 접속이 안될 수 있다는 점,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 해당 리스트에서 삭제하고자 할 때 진행이 오래 걸린다는 점 등에서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따로 HSTS Preload List에 등록하지는 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버의 경우도 최초에 접속 시 http에서 https로 리다이렉트되는 것을 보면 알 수 있듯, HSTS Preload List에 등록되지 않은 상태입니다.&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/bodZ4Y/btsIUU3FSpS/VYITGynyDktapc9GJTMcl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bodZ4Y/btsIUU3FSpS/VYITGynyDktapc9GJTMcl0/img.png&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;391&quot; data-is-animation=&quot;false&quot; style=&quot;width: 44.3361%; margin-right: 10px;&quot; data-widthpercent=&quot;44.86&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bodZ4Y/btsIUU3FSpS/VYITGynyDktapc9GJTMcl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbodZ4Y%2FbtsIUU3FSpS%2FVYITGynyDktapc9GJTMcl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;592&quot; height=&quot;391&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pP5gS/btsIUTDLJVG/vpDiclnOvsQJ8Qe3PtwTg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pP5gS/btsIUTDLJVG/vpDiclnOvsQJ8Qe3PtwTg0/img.png&quot; data-origin-width=&quot;590&quot; data-origin-height=&quot;317&quot; data-is-animation=&quot;false&quot; style=&quot;width: 54.5011%;&quot; data-widthpercent=&quot;55.14&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pP5gS/btsIUTDLJVG/vpDiclnOvsQJ8Qe3PtwTg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpP5gS%2FbtsIUTDLJVG%2FvpDiclnOvsQJ8Qe3PtwTg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;590&quot; height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;우측은 HSTS Preload List에 등록된 페이스북.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/HTTP_cookie&quot;&gt;https://en.wikipedia.org/wiki/HTTP_cookie&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;&lt;a href=&quot;https://yozm.wishket.com/magazine/detail/1862/&quot;&gt;https://yozm.wishket.com/magazine/detail/1862/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zerocho.com/category/HTML&amp;amp;DOM/post/5918515b1ed39f00182d3048&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.zerocho.com/category/HTML&amp;amp;DOM/post/5918515b1ed39f00182d3048&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;</description>
      <category>백엔드 개발하며 작성한/기억할 내용</category>
      <author>뭉지(moonz)</author>
      <guid isPermaLink="true">https://thisisprogrammingworld.tistory.com/244</guid>
      <comments>https://thisisprogrammingworld.tistory.com/244#entry244comment</comments>
      <pubDate>Sun, 7 Jul 2024 14:53:50 +0900</pubDate>
    </item>
    <item>
      <title>[SQL, 프로그래머스] 주문량이 많은 아이스크림들 조회하기</title>
      <link>https://thisisprogrammingworld.tistory.com/243</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Lv4 '주문량이 많은 아이스크림들 조회하기' 문제를 풀어보았습니다.&lt;br /&gt;구체적인 프로그래머스 문제는 &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/133027&quot;&gt;링크&lt;/a&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;참고 바라며, 문제는 간략히 설명합니다.&lt;/p&gt;
&lt;figure id=&quot;og_1718520899450&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/133027&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/e0cOM/hyWoCNX2CS/SQtadvaX0GoPjcSluUb070/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/kpcyh/hyWlccj8mT/dnCMP4xwV3FUzdB2ccAfSK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/133027&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/133027&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/e0cOM/hyWoCNX2CS/SQtadvaX0GoPjcSluUb070/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/kpcyh/hyWlccj8mT/dnCMP4xwV3FUzdB2ccAfSK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.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;h2 data-ke-size=&quot;size26&quot;&gt;사용할 테이블 정보&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 테이블 모두 아이스크림 가게의 주문 정보를 담은 테이블입니다.&lt;br /&gt;&lt;code&gt;first_half&lt;/code&gt; 테이블은 상반기의 아이스크림 주문 정보를 담은 테이블. (flavor 칼럼이 기본 키, shipment_id 칼럼이 july 테이블의 shipment_id의 외래 키)&lt;br /&gt;&lt;code&gt;july&lt;/code&gt; 테이블은 7월의 아이스크림 주문 정보를 담은 테이블입니다. &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7월에는 워낙 아이스크림 주문량이 많아서 같은 아이스크림 맛이지만, 다른 두 공장에서 아이스크림 가게로 출하를 진행하는 경우가 있습니다.&lt;br /&gt;즉, 같은 맛일지라도 출하 번호가 다른 데이터가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 테이블 모두 출하 번호, 아이스크림 맛, 총 주문량 칼럼을 갖고 있다.&lt;/p&gt;
&lt;h1&gt;생각의 과정&lt;/h1&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&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;두 테이블을 JOIN 하고, 이때 ON 절에는 flavor 칼럼으로 조건을 걸어야겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;july&lt;/code&gt; 테이블은 flavor가 중복인 케이스가 있으니 &lt;code&gt;GROUP BY flavor&lt;/code&gt;를 통해&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQL 쿼리 작성 (MySQL)&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT flavor
FROM (
    SELECT first_half.flavor, SUM(first_half.total_order) + SUM(july.total_order) AS sum_total_order
    FROM first_half
    JOIN july
    ON first_half.flavor = july.flavor
    GROUP BY flavor
) AS total
ORDER BY sum_total_order desc
LIMIT 3
;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다른 방식의 쿼리문 (MySQL)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 내용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JOIN 대상 테이블을 미리 &lt;code&gt;GROUP BY&lt;/code&gt;한 july 테이블을 조인 대상으로 걸었다.&lt;/li&gt;
&lt;li&gt;총 주문량의 합을 &lt;code&gt;SELECT&lt;/code&gt; 대상으로 두지 않고 &lt;code&gt;ORDER BY&lt;/code&gt;절에서만 사용하도록 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT j.flavor
FROM first_half
JOIN (
    SELECT flavor, SUM(total_order) AS total_order
    FROM july
    GROUP BY flavor
) AS j
ON first_half.flavor = j.flavor
GROUP BY flavor
ORDER BY first_half.total_order + j.total_order desc
LIMIT 3;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필요한 지식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN, SUM, GROUP BY, ORDER BY, LIMIT&lt;/p&gt;</description>
      <category>Algorithm &amp;amp; Data Structure/문제 풀이</category>
      <category>group by</category>
      <category>join</category>
      <category>lv4</category>
      <category>Programmers</category>
      <author>뭉지(moonz)</author>
      <guid isPermaLink="true">https://thisisprogrammingworld.tistory.com/243</guid>
      <comments>https://thisisprogrammingworld.tistory.com/243#entry243comment</comments>
      <pubDate>Sun, 16 Jun 2024 15:54:36 +0900</pubDate>
    </item>
    <item>
      <title>예외(Exception) 처리</title>
      <link>https://thisisprogrammingworld.tistory.com/241</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 예외 핸들링은 애플리케이션에 필수적이지만 생각보다 중요성을 간과할 수 있는 부분이라 생각합니다.&lt;br /&gt;이&amp;nbsp;글에서는 &lt;b&gt;예외를 다룰 때마다 고민했던 포인트들과 예외를&amp;nbsp;처리하고&amp;nbsp;생성하는&amp;nbsp;Best&amp;nbsp;Practice&lt;/b&gt;에 대해 정리합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 예외 처리 (Handling Exceptions) 할 때&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메서드가&amp;nbsp;에러로&amp;nbsp;인해&amp;nbsp;완료되지&amp;nbsp;않았을&amp;nbsp;때&amp;nbsp;상태를&amp;nbsp;복구한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드에서 예외가 발생했을 때, 호출자는 부작용이 없다고 가정할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계좌 이체 코드 (한 계좌에서 출금하고 다른 계좌에 입금하는 코드)를 예로 들면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출금 로직은 정상적으로 성공했는데 입금 로직에서 예외가 발생하면, 출금이 그대로 완료되지 않도록 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 처리할 수 있는 방안은, 입금 거래 과정 중 한 곳이라도 예외가 발생하면 그전에 정상적으로 완료한 출금을 롤백하도록 하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롤백 방식은 코드의 상황에 따라 다를 수 있겠지만, 여기에서는 try-catch문의 catch문에서 롤백 처리하도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입금 로직을 try 문으로 감싸고 예외가 발생하면 catch 문에서 출금을 롤백한 후에, 예외를 던지도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 예외는 새로 정의한 커스텀 예외를 던지면서 원래 예외를 내부 예외에 포함키도록 하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719821342100&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static void TransferFunds(Account from, Account to, decimal amount) {
    String withdrawalTrxID = from.withdrawal(amount);
    try {
        to.deposit(amount);
    }
    catch (Exception ex) {
        from.RollbackTransaction(withdrawalTrxID);
        throw CustomException(ex);
    }
}&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;h3 data-ke-size=&quot;size23&quot;&gt;예외를&amp;nbsp;적절하게&amp;nbsp;잡아서&amp;nbsp;다시&amp;nbsp;던진다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 발생하면, 그 정보들의 일부가 스택 트레이스 (Stack trace)에 담깁니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택 트레이스는 예외를 발생시킨 메서드에서 시작해서 예외를 잡는 메서드에서 끝나는,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드의 호출 계층 목록입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 어디에선가 예외가 발생했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 예외를 잡을 때, 기존 예외의 정보를 담지 않고 새로운 예외로 다시 던지게 되면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택 트레이스가 현재 메서드에서 다시 시작되기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외를 처음 던진 원래 메서드와 현재 메서드 사이의 스택 트레이스 (메서드 호출 목록)이 사라지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국! 발생한 Exception의 Stack trace 정보를 유지하려면 기존 Exception을 인자로 전달해야 하는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용은 아래 3-2에서 다시 다루겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;예외를 잡아서 다시 던질 때는 기존의 예외 정보도 함께 포함시켜 던져야 한다&quot;가 포인트입니다.&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;2) 예외 발생 (Throwing Exceptions) 시킬 때&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미 정의된 exception을 적용하지 못할 때만 새 Exception 클래스를 도입하자.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ex)&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;letter-spacing: 0px;&quot;&gt;InvalidOperationException 예외를 발생시킨다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;만약 부적절한 파라미터를 전달받았다면, ArgumentException 예외 또는 ArgumentException에서 파생된 미리 정의된 클래스 중 하나를 발생시킨다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;**이때 예약된 예외 클래스 (reserved exception) 는 발생시키지 않도록 주의해야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;reserved exception이란, 일반적으로 시스템 수준의 예외에 해당됩니다.&lt;br /&gt;이러한 예외는 사용자가 재정의하거나 확장하지 않는 것을 권장하고 있습니다.&lt;br /&gt;Java에서는 `java.lang.Exception`이 해당.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.25em; letter-spacing: -1px;&quot;&gt;Exception Builder 메서드를 사용하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 프로젝트에서 같은 예외를 각기 다른 위치에서 던지는 것은 흔하게 볼 수 있는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 null이거나 비어있는지 체크하고 비어있으면 예외를 던지는 로직만 생각해도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 곳에서 중복 코드가 발생할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 예외를 생성해서 반환하는 Helper 메서드를 별도로 생성해서 이러한 중복 코드를 제거할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 null이거나 비어있는 경우 IllegalArgumentException을 던지려고 하는 로직을 작성해보면 이렇게 될 것 같아요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 값이 있는지 체크해야하는 곳곳에서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이런 코드들이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;중복적으로 구현됩니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1720166172580&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ExampleClass {

    public void performAction(String param) {
        if (value == null || value.isEmpty()) {
            throw new IllegalArgumentException(parameterName + &quot; cannot be null or empty&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;이러한 중복 코드를 Helper 메서드로 추출하게 되면??&lt;/p&gt;
&lt;pre id=&quot;code_1720168625928&quot; class=&quot;cs&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class ArgumentValidator {

    public static void throwIfNullOrEmpty(String value, String parameterName) {
        if (value == null || value.isEmpty()) {
            throw new IllegalArgumentException(parameterName + &quot; cannot be null or empty&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;/p&gt;
&lt;pre id=&quot;code_1720167950220&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ExampleClass {

    public void performAction(String param) {
        ArgumentValidator.throwIfNullOrEmpty(param, &quot;param&quot;);
        // ..
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그외&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 적절한 문법을 사용한다. (영어를 사용할 때 특히 더 중요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. finally 문에서는 throw 예외를 두지 말자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 예상치 못할 곳에서는 예외를 던지지 말자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;- equals(), toString(), static 생성자 와 같은 메서드에서는 예외를 던지지 않는다&lt;/i&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커스텀 예외 클래스를 생성할 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 사용자 정의 예외는 -Exception 으로 끝나도록 네이밍하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 사용자 정의(user-defined) 예외 클래스를 만들 때 3개의 생성자는 기본적으로 생성하자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Exception() : Default.&lt;/li&gt;
&lt;li&gt;Exception(String) : 메시지를 전달한다.&lt;/li&gt;
&lt;li&gt;Exception(String, Exception) : 메시지와 내부 예외를 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 필요할 때 추가 프로퍼티를 제공하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 정보가 필요한 시나리오가 있을 때에만 프로퍼티를 추가하는 것을 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ex) FileNotFoundException은 &lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start;&quot;&gt;&lt;span&gt;'FileName' 필드를 추가로 제공합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;3. 그외 예외 처리 관련 참고하면 좋을 9가지&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;1. &lt;/span&gt;&lt;span&gt;어디에서 예외를 던져야할까?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;1) &quot;프로그램의 정상적인 흐름을 제어하기 위해&quot; 예외 던지는 방식을 사용하면 안된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예외는 오직 예외인 상황에서 사용해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만약 외부 API를 이용해 데이터를 조회했는데 데이터가 존재하지 않는 경우, 이 메서드에서는 예외를 발생시키기 보다 해당 데이터를 전달하는 것이 적절하다.&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;style2&quot;&gt;단, 데이터가 존재하지 않을 때 Null 값 보다는 Null Object pattern을 활용해서 결과를 반환하는 것이 좋다.&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&gt;그러면 이 데이터를 받은 호출자 메서드에서 그 결과를 확인하고 적절한 액션을 취하면 되는것이다.&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;style2&quot;&gt;호출자에서 적절한 액션을 취하는 것에는&lt;br /&gt;&lt;span&gt;에러를 커스텀 에러로 변환하여 에러를 던질 수도 있고,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;에러까지 발생시키지 않아야 하는 경우라면 간단한 로깅만 남기고 로직을 넘어갈 수 있다.&lt;br /&gt;&lt;/span&gt;&lt;span&gt;따로 에러를 커스텀 에러로 변환하지 않아도 되는 경우라면&lt;br /&gt;&lt;/span&gt;&lt;span&gt;아래 2) 처럼 가장 최상위 계층에서 예외를 처리하도록 할 수도 있다.&lt;br /&gt;&lt;/span&gt;&lt;span&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&gt;&lt;b&gt;2) 가능한 늦게 예외를&amp;nbsp;처리 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Exception을 throw 하자마자 잡지 않고, 가능한 가장 최상위 계층에서 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;아래와 같이 &lt;/span&gt;&lt;span&gt;divide()&lt;/span&gt;&lt;span&gt; 에서 b == 0일 때 던진 에러를, &lt;/span&gt;&lt;span&gt;calculate()&lt;/span&gt;&lt;span&gt;에서 예외를 처리하게 되면 모든 &lt;/span&gt;&lt;span&gt;divide()&lt;/span&gt;&lt;span&gt; 를 호출하는 곳에서는 에러를 처리해야할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;좋지 않은 예외 처리&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1718350475713&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void divide(int a, int b) {
    if (b == 0) {
    	throw new DivideZeroError(&quot;Cannot divided by zero.&quot;);
	}
    // .. 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1718350498022&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void calculate() {
  try {
    divide(Math.random(), Math.random());
  } catch (Exception e) {
    if (error instanceof DivideZeroError) {
      // ...
    }
  }
}&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&gt;적절한 예외 처리&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1718350535679&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void divide(int a, int b) {
  if (b == 0) {
    throw new DivideZeroError(&quot;Cannot divided by zero.&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1718350569491&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void calculate() {
    divide(Math.random(), Math.random());
}&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&gt;그리고 글로벌 핸들러와 같은 최상위 계층에서 예외 처리를 하면 가독성도 좋아지고 코드를 재사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1718350587462&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExceptionHandler(DivideZeroError.class)
public void divideZeroErrorHandler(DivideZeroError error) {
  // 로깅
  // 사용자에게 응답할 에러 response 세팅
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&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&gt;애플리케이션을 서비스하면, 수많은 요청이 동시에 들어오기 때문에 여러 줄의 로그로 남기게 되면 다른 로그들과 섞여 로그 간 멀어지게 되어 보기 어려울 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋지 않은 예외 처리&lt;/p&gt;
&lt;pre id=&quot;code_1718351095241&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
  //...
} catch(Exception ex) {
  log.error(&quot;error1&quot;);
  log.error(&quot;error2&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 예외 처리&lt;/p&gt;
&lt;pre id=&quot;code_1718351129483&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
  //...
} catch(Exception ex) {
  log.error(&quot;error1. error2&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;&lt;b&gt;3. 발생하는 예외를 커스텀 예외로 전환시킬 때에는 기존 예외의 원인(Throwable)을 인자로 받는 것이 좋다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생한 Throwable을 파라미터를 통해 가져올 수 있는 생성자를 최소한 하나를 구현하고 super 생성자에 Throwable를 전달해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1718351187304&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    // do something
} catch (NumberFormatException ex) {
    throw new CustomException(&quot;A message that describes the error.&quot;, ex, ErrorCode.INVALID_PORT_CONFIGURATION);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1718351219894&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BusinessException extends Exception {
    public BusinessException(String message, Throwable cause, ErrorCode code) {
            super(message, cause);
            this.code = code;
    }
}&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;특히 이 사항은 직접 경험하며 느낀 내용인데요, 기존 예외의 원인(Throwable)을 인자로 전달한 상황과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 전달하지 않은 상황에는 에러를 디버깅하고 원인을 찾아가는 과정에 차이가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러가 발생하는 상황은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 로그인 클래스 A의 메서드에서 쿠키를 처리하는 유틸 클래스 B 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 유틸 클래스 B의 메서드에서 IllegalArgumentException이 발생.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 로그인 클래스 A의 메서드 내에서 2)번 호출 로직에 대한 try/catch문 존재&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719276948602&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
	CookieUtils.setValidateCookie(generatedTokens);
} catch (Exception ex) {
	throw new CookieProcessFailException(String.format(&quot;cause: %S&quot;, ex.getClass()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결&lt;/p&gt;
&lt;pre id=&quot;code_1719276962645&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
	CookieUtils.setValidateCookie(generatedTokens);
} catch (Exception ex) {
	throw new CookieProcessFailException(String.format(&quot;cause: %S&quot;, ex.getClass()), ex);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;&lt;b&gt;4. 예외를 잡는 곳은 상황에 따라 다르다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예외가 발생한 곳에서 무조건 예외를 처리하려고 하지 않아도 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, 처리하기 적합하지 않은 곳에서 무리하게 예외 처리하려고 하지 말라는 것인데요&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 최상위 레이어에서 사용자에게 파일의 위치를 입력 받고, 최하위 레이어에서 이 파일의 내용을 읽어 드리는 어플리케이션이 있습니다. 파일이 존재하지 않는 예외의 처리 방법으로 사용자 입력을 다시 받게 하려면, 최상위 레이어가 예외를 처리해야하고 사이의 레이어들은 체이닝만 하는 것이 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;&lt;b&gt;5. 리소스는 정리해야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;try-catch 구문에서 리소스를 연다면, finally 블록에서 리소스들을 정리하거나 try-with-resource 구문을 이용해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AutoCloseable 인터페이스를 구현한 리소스라면 try-with-resource 구문을 이용하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;&lt;b&gt;6. 발생할 수 있는 예외는 문서화한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;JavaDoc을 통해 어떤 예외가 발생할 수 있는지 명시합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1718351374632&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * Member 엔티티를 조회합니다.
 * @param memberId 조회할 회원의 id
 * @throws MemberNotFoundException 조회한 회원이 없는 경우에 예외가 발생합니다.
 */
public Member findOne(String memberId) {
	return memberRepository.findByMemberId(String memberId)
    	.orElseThrow(() -&amp;gt; new MemberNotFoundException(String.format(&quot;회원 '%s'이 존재하지 않습니다.&quot;, memberId)));
}&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;size18&quot;&gt;&lt;span&gt;&lt;b&gt;7. 예외 메세지를 자세하게 적는다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사용자 정의 예외를 만들 때에는 예외가 발생한 상황에 대해 로그 메시지를 적을 수 있는데,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;어떤 상황에서 예외가 발생했는지 간결하게 적어줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; 그러나, 예외 이름 자체에서 어떤 문제로 발생했는지 알 수 있을 경우에는 너무 많은 정보를 적지 않아도 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Ex) &lt;/span&gt;&lt;span&gt;NumberFormatException&lt;/span&gt;&lt;span&gt; : 숫자 포맷이 아닌 포맷의 문자열을 숫자로 변환하려할 때 문제가 발생했음을 짐작할 수 있습니다. 이런 경우에는 어떤 문자열을 변환시키려했는지만 작성해놔도 괜찮습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;&lt;b&gt;8. 예외를 상위 메서드로 던질 때 로그를 남기지 마라&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예외가 발생한 코드에서 상위 메서드로 예외를 던질 때에는, 별도의 로그 처리를 하지 않아도 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;상위로 예외를 throw 할 경우 로그가 너무 많아져 가독성을 훼손하기 때문입니다.&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&gt;다시 상위로 throw하고자 한다면, 로그를 찍지 말고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그상황에 대한 Context를 남기고자 한다면 이를 감싸는 커스텀 클래스로 만들고, 이를 상위로 던지길 권장합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예외를 래핑하는 커스텀 클래스 예시&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1718351546067&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MyException extends BusinessException {
	public MyException(String message, Exception ex) {
		super(message, ex);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;&lt;b&gt;9. 예외를 잡은 후 처리할 때 유형&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예외를 잡았을 때 &lt;/span&gt;&lt;span&gt;catch문에서 로그만 남겨야 하는 일은 거의 드뭅니다. 예외를 잡았으면 무언가를 해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리 유형은 3가지 케이스가 있을 것 같습니다.&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&gt;처리하고 예외 상황 종료&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;다시 던진다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;새 예외로 커스텀하여 던진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;&lt;b&gt;10. 로그 메시지는 어떻게 남기는게 좋을까?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;lt;발생 상황 + 어떤 Exception인가 + Exception 메세지&amp;gt; 정도만 알려주어도 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;더 나아가서는, &amp;lt;발생 IP + 발생 Port + 발생 스레드&amp;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&gt;충분한 로그 예)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &amp;ldquo;second data reading failed. cause=IOException, message=&amp;ldquo;socket read failed.&amp;rdquo;], peer=&amp;ldquo;10.10.10.13&amp;rdquo;, port=1234, auth=&amp;ldquo;base123&amp;rdquo;, thread=32&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고글&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://jojoldu.tistory.com/734&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;좋은 예외(Exception) 처리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#design-classes-so-that-exceptions-can-be-avoided&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MS 문서 - design-classes-so-that-exception-can-be-avoided&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>백엔드 개발하며 작성한/기억할 내용</category>
      <author>뭉지(moonz)</author>
      <guid isPermaLink="true">https://thisisprogrammingworld.tistory.com/241</guid>
      <comments>https://thisisprogrammingworld.tistory.com/241#entry241comment</comments>
      <pubDate>Fri, 14 Jun 2024 16:53:33 +0900</pubDate>
    </item>
    <item>
      <title>[SQL, 프로그래머스] 상품 별 오프라인 매출 구하기</title>
      <link>https://thisisprogrammingworld.tistory.com/240</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머스에 여러 문법들을 활용해서 SQL 쿼리문을 짜볼 수 있도록 다양한 문제들을 레벨 별로 제공하고 있습니다.&lt;br /&gt;Lv2 '상품 별 오프라인 매출 구하기' 문제를 풀어보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/131533&quot;&gt;프로그래머스 문제 링크&lt;/a&gt; 참고 바라며, 문제는 간략히 설명합니다.&lt;/p&gt;
&lt;figure id=&quot;og_1717909267614&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/131533&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cmvqPl/hyWg9zZzhe/W08Kgivhisj33h3zhCSBK0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bvnlvj/hyWliaW4oX/Qbwi6M8lkkXQ9rPtzjxKDK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/131533&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/131533&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cmvqPl/hyWg9zZzhe/W08Kgivhisj33h3zhCSBK0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bvnlvj/hyWliaW4oX/Qbwi6M8lkkXQ9rPtzjxKDK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.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;h2 data-ke-size=&quot;size26&quot;&gt;사용할 테이블 정보&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 의류 쇼핑몰에서 판매중인 상품들의 상품 정보를 담은 &lt;code&gt;PRODUCT&lt;/code&gt; 테이블과&lt;br /&gt;오프라인 상품 판매 정보를 담은 &lt;code&gt;OFFLINE_SALE&lt;/code&gt; 테이블이 있으며,&lt;br /&gt;두 테이블 모두 &lt;code&gt;product_id&lt;/code&gt; 칼럼을 가지고 있습니다.&lt;/p&gt;
&lt;h1&gt;생각의 과정&lt;/h1&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;product 테이블과 offline_sale 테이블 각각의 칼럼을 조합하여 결과를 보여주어야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;JOIN&lt;/code&gt;을 수행해야겠다. &lt;code&gt;product_id&lt;/code&gt;를 공통으로 가지고 있으니 이를 활용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;매출액은 &lt;code&gt;product.price&lt;/code&gt;와&amp;nbsp;&lt;code&gt;offline_sale.sales_amount&lt;/code&gt;의 곱이다.&lt;/li&gt;
&lt;li&gt;상품코드 별 매출액의 합을 구해야한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품코드로 &lt;code&gt;GROUP BY&lt;/code&gt;하여 &lt;code&gt;SUM()&lt;/code&gt;을 구해야겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;매출액으로 내림차순하고, 동일한 매출액이면 상품코드로 오름차순한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ORDER BY&lt;/code&gt; 매출액 desc, 상품코드 asc&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;(참고) 상품ID는 OFFLIE_SALE 테이블에 중복으로 존재할 수 있으나, 상품ID와 판매일까지 고려한 데이터는 중복 존재하지 않는다.&lt;/li&gt;
&lt;li&gt;상품이 오프라인 판매 테이블에 존재하지 않을 수 있고, 이럴 경우 어떻게 할지 고려해야 할 것 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(해당 내용에 대한 설명은 따로 없어 우선 고려하지 않고 문제를 제출하니 테스트 케이스에 이런 경우는 없는지 통과되었다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;SQL 쿼리 작성 (MySQL)&lt;/h1&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT product.product_code, SUM(product.price * offline_sale.sales_amount) AS sales
FROM product INNER JOIN offline_sale
ON product.product_id = offline_sale.product_id
GROUP BY product.product_code
ORDER BY sales desc, product.product_code asc;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필요한 지식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INNER JOIN, GROUP BY, ORDER BY, 연산함수 SUM&lt;/p&gt;</description>
      <category>Algorithm &amp;amp; Data Structure/문제 풀이</category>
      <author>뭉지(moonz)</author>
      <guid isPermaLink="true">https://thisisprogrammingworld.tistory.com/240</guid>
      <comments>https://thisisprogrammingworld.tistory.com/240#entry240comment</comments>
      <pubDate>Sun, 9 Jun 2024 14:00:05 +0900</pubDate>
    </item>
    <item>
      <title>[SQL, 프로그래머스] 조건에 맞는 도서와 저자 리스트 출력하기</title>
      <link>https://thisisprogrammingworld.tistory.com/239</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머스에 여러 문법들을 활용해서 SQL 쿼리문을 짜볼 수 있도록 다양한 문제들을 레벨 별로 제공하고 있습니다.&lt;br /&gt;Lv2인'조건에 맞는 도서와 저자 리스트 출력하기' 문제를 풀어보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/144854&quot;&gt;프로그래머스 문제 링크&lt;/a&gt;&amp;nbsp;참고 바라며, 문제는 간략히 설명합니다.&lt;/p&gt;
&lt;figure id=&quot;og_1717904955478&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/144854&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b8zMdQ/hyWg8VnWJa/wyqrcId30o3iesOHrso5AK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/thHbm/hyWlgc7XEd/AGAJYeWDGlH5LBkItBUIq1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/144854&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/144854&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b8zMdQ/hyWg8VnWJa/wyqrcId30o3iesOHrso5AK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/thHbm/hyWlgc7XEd/AGAJYeWDGlH5LBkItBUIq1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.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;h3 data-ke-size=&quot;size23&quot;&gt;사용할 테이블 정보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 한 서점에서 판매중인 도서 정보를 담고 있는 book 테이블과 저자 정보를 담고 있는 author 테이블이 있으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 테이블 모두 author_id 값을 갖고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고민 과정&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;book&lt;/code&gt; 테이블에 있는 정보와 &lt;code&gt;author&lt;/code&gt; 테이블에 있는 정보를 합쳐서 보여줘야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그냥 합치면 될 것으로 보이니 &lt;code&gt;INNER JOIN&lt;/code&gt;해야지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;카테고리 = '경제'로 필터링해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHERE 절 추가해야지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;book.published_date&lt;/code&gt;로 정렬해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마지막에 ORDER BY 추가해야지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그외 고려해야할 것은 없어보인다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL 짜러 고!&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ (고민 과정에서는 놓쳤지만) &lt;code&gt;published_date&lt;/code&gt;를 '2021-04-01'처럼 노출되도록 포맷팅 적용한다.&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;SQL 쿼리 작성&lt;/h2&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT book.book_id, author.author_name, DATE_FORMAT(book.published_date, '%Y-%m-%d') AS published_date
FROM book JOIN author
ON book.author_id = author.author_id
WHERE book.category = '경제'
ORDER BY book.published_date;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가 필요한 지식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INNER JOIN, ORDER BY, DATE_FORMAT()&lt;/p&gt;
&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;DATE_FORMAT()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 유형인 'JOIN' 개념 외에 Date 포맷팅 적용 방법도 알아야했습니다.&lt;br /&gt;이미 Date 타입인데, 데이터의 포맷을 yyyy-mm-dd HH:MM:SS -&amp;gt; yyyy-mm-dd 형태로 바꾸어주어야 하려면, 아래 문법을 사용합니다.&lt;br /&gt;데이터 포맷팅 규칙은 다양하게 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;DATE_FORMAT(date, '%Y-m-%d')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 포맷을 외우기 힘들다면 아래 예시 정도면 알아두어도 괜찮을 것 같아요&lt;/p&gt;
&lt;table style=&quot;height: 89px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;포맷 표현식&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;'%Y-%m-%d %H:%i:%s'&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;1999-01-01 00:00:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;'%Y-%m-%d %T'&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;1999-01-01 00:00:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;'%Y-%m-%d %r'&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;1999-01-01 12:00:00 AM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;**시각 : 정오&lt;/span&gt;&lt;/p&gt;</description>
      <category>Algorithm &amp;amp; Data Structure/문제 풀이</category>
      <category>SQL</category>
      <category>프로그래머스</category>
      <author>뭉지(moonz)</author>
      <guid isPermaLink="true">https://thisisprogrammingworld.tistory.com/239</guid>
      <comments>https://thisisprogrammingworld.tistory.com/239#entry239comment</comments>
      <pubDate>Sun, 9 Jun 2024 12:48:08 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] permitAll() 적용되지 않는 이슈 해결</title>
      <link>https://thisisprogrammingworld.tistory.com/238</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드 도메인의 애플리케이션에는 권한 있는 사용자만 접근 가능한 API 1개로만 존재했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근, 권한 체크 없이 접근 가능한 API를 한개 더 추가해야하면서 Spring Security의 설정을 수정해야 했습니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;구체적인 기능에 대한 설명&lt;/summary&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 권한이 있는 사용자에 한해 다운로드할 수 있도록 하기 위해 애플리케이션 내 API에서 요청을 처리하도록 하였고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 권한 체크가 필요 없는, 누구나 다운로드할 수 있는 파일들의 경우 Nginx에서 파일들을 나르도록 처리하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(해당 서버는 NAS 서버와 마운트 되어있었기 때문에 파일을 매핑하여 전달하는 것이 가능했습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 VM 환경에서 구동 중이었으나 컨테이너 환경으로 전환해야하면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx가 수행하던 역할을 애플리케이션 내에서 처리하기로 결정하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다운로드 애플리케이션 내에 적용되어 있는 Spring Security에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 존재하던 API에 대해서만 권한 체크를 수행하고, 그외 API는 권한없이도 접근할 수 있도록 설정을 추가하기로 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 Security Filter를 잘못 사용되고 있는 부분이 있었고, 이를 해결하면면서 알게된 내용을 글로 정리합니다.&lt;/p&gt;
&lt;/details&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Config 클래스에서 기존에는 모든 API에 대해 권한 체크가 필요하도록 설정되어 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 SecurityConfig 클래스&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final ObjectMapper objectMapper;

    private final AuthenticationJwtTokenProvider authenticationJwtTokenProvider;

    private final UserDetailService userDetailService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().configurationSource(corsConfigurationSource())
                .and()
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()  // 기존 설정
                .and()
                .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
                .and()
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .and()
                .addFilterBefore(new AuthenticationTokenFilter(authenticationJwtTokenProvider, userDetailService), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtExceptionFilter(objectMapper), AuthenticationTokenFilter.class)
        ;

    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        // 생략
    }

    @Override
    public void configure(WebSecurity web) {
        // 생략
    }
}&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;그런데 신규로 등록한 API는 권한 체크 없이 접근하도록 설정해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 애플리케이션에 존재하는 2개의 API 중 1개(&lt;code&gt;/api/downloads/**&lt;/code&gt;)는 권한 체크를 해야하고, 나머지 1개는 누구나 접근할 수 있도록 해야하는 상황이 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누구나 접근할 수 있도록 한 API를 'any접근 API'라고 부르도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정한 모습입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 수정한 일부만 발췌
                .authorizeRequests()
                .antMatchers(&quot;/api/downloads/**&quot;).authenticated()
                .anyRequest().permitAll()
                .and()&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;이제 'any접근 API'를 호출하면 권한이 없어도 파일을 다운로드받을 수 있을 것이라 예측했는데, 애플리케이션에서는 401 Unauthorized&amp;nbsp; 상태코드와 함께 JWT 토큰이 없다는 에러를 반환해버립니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Filter를 디버깅해보니, Filter Chain 중 AuthenticationTokenFilter 클래스 내 JWT 토큰을 체크하는 로직에서 토큰이 없어서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;HttpServletResponse 에 에러 정보(상태, 에러 메시지)를 세팅하고 &lt;/span&gt;&lt;code&gt;return;&lt;/code&gt;문을 통해 클라이언트에게 바로 응답 하도록 구현되어 있었습니다.&lt;/p&gt;
&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;기존 Security Filter&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
public class AuthenticationTokenFilter extends OncePerRequestFilter {

    private final AuthenticationJwtTokenProvider authenticationJwtTokenProvider;
    private final UserDetailService userDetailService;

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

        try {
            Optional&amp;lt;String&amp;gt; token = authenticationJwtTokenProvider.findToken(request);
            // 토큰이 없으면 에러로 세팅하고 필터 중단.
            // 여기서 에러 발생 후 응답 반환.
            if (token.isEmpty()) {
                doErrorHandle(request, response, &quot;Jwt must not null&quot;);
                return;
            }

            Optional&amp;lt;UserDetailDto&amp;gt; userToken = authenticationJwtTokenProvider.getUserToken(token.get());
            // 토큰이 없으면 에러로 세팅하고 필터 중단.
            if (userToken.isEmpty()) {
                doErrorHandle(request, response, String.format(&quot;Failed translate Jwt to UserDetailDto (token:%s)&quot;, token));
                return;
            }

            UserDetailDto userDetailDto = userToken.get();
            // 사용자가 유효하지 않으면 에러로 세팅하고 필터 중단.
            if (!validateUserDetail(userDetailDto)) {
                doErrorHandle(request, response, String.format(&quot;Invalid UserDetailDto (userDetailDto:%s)&quot;, userDetailDto.toString()));
                return;
            }

            UserDetails userDetail = userDetailService.loadUserDetail(userDetailDto);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetail, &quot;&quot;, userDetail.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        } catch (InvalidAuthenticationException exception) {
            // Exception 발생하면 에러로 세팅하고 필터 중단.
            log.error(ExceptionUtils.getStackTrace(exception));
            doErrorHandle(request, response, &quot;Invalid UserDetail&quot;);
            return;
        }

        filterChain.doFilter(request, response);

    }

    private boolean validateUserDetail(UserDetailDto userDetailDto) {
        // true or false 반환
    }

    private void doErrorHandle(HttpServletRequest request, HttpServletResponse response, String message) {
        try {
            log.error(String.format(&quot;[authentication token] {msg=%s, url=%s, remote_ip=%s}&quot;,
                    message, request.getRequestURI(), request.getRemoteAddr()));

            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, message);

        } catch (IOException exception) {
            log.error(ExceptionUtils.getStackTrace(exception));
        }
    }&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;사실 저는 'any접근 API'를 호출하면 아예 시큐리티 필터를 안 탈 것이라 생각했는데 Security Filter를 타서 조금 놀랐습니다.(...)&lt;/p&gt;
&lt;p data-ke-size=&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;결국 SecurityConfig 클래스에서 permitAll()로 설정한 URI에 상관없이&amp;nbsp;위 클래스에서 &lt;b&gt;필터를 중단&lt;/b&gt;시키는 것이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;401 Unauthorized 에러의 원인이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방향과 간단한 흐름 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제를 해결하면서 Security Filter를 디버깅해보며 흐름을 확인하니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 SecurityConfig에 원하는 설정 정보들을 지정하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이용해서 각 요청들에 대해 승인/거부하는  것을 수행하는 필터가 Security Filter Chain 중 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 SecurityConfig에 이미 각 URI에 대해 권한 설정을 해놓았다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Filter Chain 내에서 요청 URI에 대해 승인/거부 여부를 판단할 수 있도록 맡겨놓으면 되는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;승인/거부에 따른 후처리 로직에 대해 커스텀하고 싶다면, 디폴트 클래스를 상속하여 커스텀할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최종 해결한 방법은,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthenticationJwtFilter에서 아래의 경우와 같이 인증/인가에 이슈가 있어도 &lt;b&gt;필터를 중간에 중 지시키지 않도록 하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 헤더 or 쿠키에 토큰이 없는 경우&lt;/li&gt;
&lt;li&gt;토큰이 있으나, 토큰 내 정보를 까보니 유의미한 사용자가 아닌 경우&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 당연하게도 위 케이스에 해당하면 Authentication 객체를 세팅하지 못하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 Security Filter에서 인증/인가를 승인할지 거부할지 심사하는 클래스(&lt;span data-token-index=&quot;0&quot;&gt;AccessDecisionVoter&lt;/span&gt;)에서 아래 정보들을 보고 심사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심사할 때 고려하는 값:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authentication - 인증 정보 (user)&lt;/li&gt;
&lt;li&gt;FilterInvocator (요청 정보)와 ConfigAttributes (권한 정보) : Security Config에 설정한 URI 와 그 URI에 세팅한 권한 정보&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;b&gt;남은 이야기&lt;/b&gt;&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;WebSecurity 클래스의 ignoring() 메서드에 추가할 순 없었나?&lt;/summary&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 아예 필터를 타지 않도록 WebSecurity 에 해당 uri를 추가하고자 했으나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'any 접근 API'가 &lt;code&gt;/api/downloads/**&lt;/code&gt; uri 외의 모든 요청에 대해 처리해야하는 API였기 때문에 컨트롤러 모습이 아래와 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;/**&lt;/code&gt; 패턴을 추가하면, 권한 체크를 해야하는 &lt;code&gt;/api/downloads/&lt;/code&gt; API마저 권한 없이 접근이 가능해져버려서 해당 부분에 세팅할 수는 없었습니다.&lt;/p&gt;
&lt;/details&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;@Getmapping(&quot;/**&quot;)
public void noAuthFile(HttpServletRequest request, HttpServletResponse response, ...) {
    // 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;문제를 해결하는 과정에서 알게된 Spring Security Filter에 대한 내용은 다른 글에서 정리해보도록 하겠습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TroubleShooting</category>
      <author>뭉지(moonz)</author>
      <guid isPermaLink="true">https://thisisprogrammingworld.tistory.com/238</guid>
      <comments>https://thisisprogrammingworld.tistory.com/238#entry238comment</comments>
      <pubDate>Tue, 21 May 2024 22:53:58 +0900</pubDate>
    </item>
  </channel>
</rss>