<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Janu is Developing</title>
    <link>https://janudev.tistory.com/</link>
    <description>됬어요 피 수혈 안해줘요</description>
    <language>ko</language>
    <pubDate>Tue, 7 Apr 2026 07:46:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>JanuDev</managingEditor>
    <image>
      <title>Janu is Developing</title>
      <url>https://tistory1.daumcdn.net/tistory/7477204/attach/02dd011fbac94eadbbbc674336737b70</url>
      <link>https://janudev.tistory.com</link>
    </image>
    <item>
      <title>[Dalmuri] 맡김 API 호출해서 감정 분석하기 - WebClient를 이용해서</title>
      <link>https://janudev.tistory.com/60</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://janudev.tistory.com/37&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;부제: 요즘은 돈없으면 개발도 못한다.&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775193076167&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;[Dalmuri] Java로 Google Cloud Nature Language API 호출해서 감정 다이어리 만들기&quot; data-og-description=&quot; API 연결 후기 : 코드 짜는것보다 초기 세팅이 훨씬 더 어렵다.. 여럽다기보단 방법도 많고 읽어야 하는 공식 문서도 많아서, 정작 내가 원하거나 필요한 정보를 찾는데 더 시간이 오래 걸린 것&quot; data-og-host=&quot;janudev.tistory.com&quot; data-og-source-url=&quot;https://janudev.tistory.com/37&quot; data-og-url=&quot;https://janudev.tistory.com/37&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bvwaML/dJMb82MDvsv/277CUEcHTBAiFd96xW70S1/img.jpg?width=800&amp;amp;height=855&amp;amp;face=255_62_606_445,https://scrap.kakaocdn.net/dn/dIojkM/dJMb87f6TEi/PXMzRNJM3g4ztbQdDPK2nk/img.jpg?width=800&amp;amp;height=855&amp;amp;face=255_62_606_445,https://scrap.kakaocdn.net/dn/cAY5N7/dJMb82eNrS7/AJyflWsrSAKK47AtDZY1hk/img.png?width=1075&amp;amp;height=742&amp;amp;face=0_0_1075_742&quot;&gt;&lt;a href=&quot;https://janudev.tistory.com/37&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://janudev.tistory.com/37&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bvwaML/dJMb82MDvsv/277CUEcHTBAiFd96xW70S1/img.jpg?width=800&amp;amp;height=855&amp;amp;face=255_62_606_445,https://scrap.kakaocdn.net/dn/dIojkM/dJMb87f6TEi/PXMzRNJM3g4ztbQdDPK2nk/img.jpg?width=800&amp;amp;height=855&amp;amp;face=255_62_606_445,https://scrap.kakaocdn.net/dn/cAY5N7/dJMb82eNrS7/AJyflWsrSAKK47AtDZY1hk/img.png?width=1075&amp;amp;height=742&amp;amp;face=0_0_1075_742');&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;[Dalmuri] Java로 Google Cloud Nature Language API 호출해서 감정 다이어리 만들기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt; API 연결 후기 : 코드 짜는것보다 초기 세팅이 훨씬 더 어렵다.. 여럽다기보단 방법도 많고 읽어야 하는 공식 문서도 많아서, 정작 내가 원하거나 필요한 정보를 찾는데 더 시간이 오래 걸린 것&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;janudev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 드럽고 치사한 세상이다... 거렁뱅이 개발자들은 개발 어떻게 하라고 맨날 과금타령 하는질 모르겠다. 난 클로드 코드도 돈아까워서 그냥 잼민이+클로드 고문하고 있는데...인텔리제이 1년치 요금도 대학교 계정으로 40퍼 할인받는것도 덜덜 떨면서 결제하고 있는데... 그리고 게시글 작성 기준일로 환율도 장난아니게 높아서 가격 더올랐을거다...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;322&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boyJJy/dJMb990eid8/v0DRUnq3GOu41horl29KbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boyJJy/dJMb990eid8/v0DRUnq3GOu41horl29KbK/img.png&quot; data-alt=&quot;우리나라 환율도 박살났는데 일본은 섭종하나요,,,,?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boyJJy/dJMb990eid8/v0DRUnq3GOu41horl29KbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboyJJy%2FdJMb990eid8%2Fv0DRUnq3GOu41horl29KbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;322&quot; height=&quot;176&quot; data-origin-width=&quot;322&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;우리나라 환율도 박살났는데 일본은 섭종하나요,,,,?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;565&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH2Xoo/dJMcaaEPrEr/DytauOOKRIlxyu3NXpk5Q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH2Xoo/dJMcaaEPrEr/DytauOOKRIlxyu3NXpk5Q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH2Xoo/dJMcaaEPrEr/DytauOOKRIlxyu3NXpk5Q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH2Xoo%2FdJMcaaEPrEr%2FDytauOOKRIlxyu3NXpk5Q1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;457&quot; height=&quot;331&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;565&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;근데 그 타이밍에 API가 고장나서 문의를 드렸고 다른 대체품이 있을까 검색하다가 Google 자연어 분석 API를 알게 되었고... 그걸 활용하려고  고생 하면서 Google colud에 내 프로젝트 생성했고.. 권한 설정했고.. pom.xml에 의존성 추가하고 어쩌구 저쩌구 한건데...&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;468&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WkbBz/dJMcaaEPshn/2boapGlnekKoPRFHlMx7pK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WkbBz/dJMcaaEPshn/2boapGlnekKoPRFHlMx7pK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WkbBz/dJMcaaEPshn/2boapGlnekKoPRFHlMx7pK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWkbBz%2FdJMcaaEPshn%2F2boapGlnekKoPRFHlMx7pK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;291&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 여러가지 공부했다고 정신승리하련다...&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 맡김 API란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://matgim.ai/public-api/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://matgim.ai/public-api/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://%20https://www.datansoft.com/product&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 데이탄소프트&lt;/a&gt;라는 회사에서 출시한 API 서비스인데, 다양한 인공지능 API를 사용할 수 있는 서비스라고 한다. 실제로 감정 분석 API 말고도 다양한 API들이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0a8Ja/dJMcagdXWHU/Hu827p247S71NbQ9i8KLs1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0a8Ja/dJMcagdXWHU/Hu827p247S71NbQ9i8KLs1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0a8Ja/dJMcagdXWHU/Hu827p247S71NbQ9i8KLs1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b0a8Ja/dJMcagdXWHU/Hu827p247S71NbQ9i8KLs1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;830&quot; height=&quot;789&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;789&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;완벽한 무료는 아니고, 한달에 100회 이상은 만원 + a로 과금이 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d22tMc/dJMcabjrg8o/OvA2AT1odctyvpEFklENJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d22tMc/dJMcabjrg8o/OvA2AT1odctyvpEFklENJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d22tMc/dJMcabjrg8o/OvA2AT1odctyvpEFklENJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd22tMc%2FdJMcabjrg8o%2FOvA2AT1odctyvpEFklENJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;867&quot; height=&quot;251&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 플젝을 사용할 나나 내 친구들이 매일매일 일기를 쓸 정도로 부지런하지 않기 때문에(ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ) 안심하고 과금 버전 사용할 수 있을거다. 친구들아 고마워!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 개발 순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Google 자연어 분석 API를 싹 다 주석처리하고 거기에 새로이 맡김 API를 사용할 것이기 때문에 controller, service는 새로이 생성될 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Google에선 자체적으로 의존성을 추가시켜서 객체를 본인들이 생성해줬는데(Sentiment, LanguageServiceClient와 같이)&lt;/p&gt;
&lt;p data-ke-size=&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;(1) MatgimResponseDTO 객체 생성&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;353&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckSlJX/dJMcafMUBFJ/UcwXpkqEA3MsXxVEiasq7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckSlJX/dJMcafMUBFJ/UcwXpkqEA3MsXxVEiasq7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckSlJX/dJMcafMUBFJ/UcwXpkqEA3MsXxVEiasq7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckSlJX%2FdJMcafMUBFJ%2FUcwXpkqEA3MsXxVEiasq7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;434&quot; data-origin-width=&quot;353&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맡김 API에서 다음과 같은 json 형식으로 가져오기 때문에 이것과 동일한 구조의 DTO를 만든다.&lt;/p&gt;
&lt;pre id=&quot;code_1775194054799&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.*;
import java.util.List;

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class MatgimResponseDTO {
    private boolean success;
    private Result result;

    @Getter
    @Setter
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
    public static class Result {
        private DocumentScore documentScore;
        private List&amp;lt;SentenceScore&amp;gt; sentenceScore;
    }

    @Getter
    @Setter
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
    public static class DocumentScore {
        private double score;
        private double posScore;
        private double negScore;
    }

    @Getter
    @Setter
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
    public static class SentenceScore {
        private String sentence;
        private double score;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;427&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2icPB/dJMcafe5h3N/QbAIpPCK6b0Z8M5FXnT8n0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2icPB/dJMcafe5h3N/QbAIpPCK6b0Z8M5FXnT8n0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2icPB/dJMcafe5h3N/QbAIpPCK6b0Z8M5FXnT8n0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2icPB%2FdJMcafe5h3N%2FQbAIpPCK6b0Z8M5FXnT8n0%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;469&quot; height=&quot;313&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;427&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 Java객체의 camelCase와 Json 객체의 snake_case를 서로 매핑시켜주는 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 들어오는 snake_case형식의 Json 데이터를 Java의 caleCase로 필드에 자동으로 매핑(역직렬화)해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 클래스는&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775197394464&quot; class=&quot;java&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 MatgimResponseDTO {  // 최상위 부모 클래스
    // 내부 클래스
    class Inner {}          // ❌ static 없음
    static class Inner2 {}  // ✅ static 있음
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 구조이다.&amp;nbsp;&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;public class MatgimResponseDTO(최상위 클래스) : 부모 클래스라는 개념이 없음. 독립적인 파일 단위, 어디서든 new MatgimResopnseDTO()로 바로 생성할 수 있음.&lt;/li&gt;
&lt;li&gt;public class inner : MatgimResponseDTO라는 '집' 안에서만 살 수 있는 클래스. 반드시 MatgimResponseDTO가 먼저 생성되어야 한다. &lt;span&gt;static이 없는 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;내부 클래스일 때만(=public class Inner일때만)&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;외부 클래스 객체(MatgimResponseDTO)가 있어야 생성할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;public static class Result(내부 클래스에 static이 있는 경우) : 이름표는 MatgimResponseDTO.Result를 달고 있지만 실제론 new MatgimResponseDTO.Result()로 혼자 태어날 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 public class와 public static class의 차이는?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;public class : 외부 클래스의 인스턴스가 먼저 생성되야만 존재할 수 있는 클래스. &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;단 최상위 클래스의 경우 외부 클래스라는 개념도 없다(ex. MatgimResponseDTO)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;public static class : public class 내부에서만 쓸 수 있는 정적 클래스. 여기선 public Static class Result, public static class DocumentScore, public static class SentenceScore가 있다. 일반 클래스처럼 혼자 생성 가능하며 주로 DTO 계층 구조를 만들 때 권장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;i&gt;&lt;u&gt;&lt;b&gt;&quot;내부 클래스일 때&quot;&lt;/b&gt;&lt;/u&gt;&lt;/i&gt; &lt;b&gt;static 없으면 &amp;ldquo;부모 없이는 못 태어남&amp;rdquo; &amp;rarr; MatgimResponseDTO에선 해당 없음(애초에 내부 클래스가 아니기 때문)&lt;/b&gt;&lt;br /&gt;  &lt;b&gt;static 있으면 &amp;ldquo;혼자 태어날 수 있음&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775197039982&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MatgimResponseDTO
 └── Result
      └── DocumentScore
      └── SentenceScore&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 MatgimResponseDTO는 최상위 클래스로 부모가 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Result, DocumentScore, SentenceScore은 MatgimResponseDTO 안에 포함된 내부 클래스.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Result 나 DocumentScore을 static으로 선언해야 하는 이유는&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; : Jackson이 JSON을 Java객체로 바꿀 때, 외부 클래스인 MatgimResponseDTO를 먼저 만들지 않고도 내부의 Result객체를 생성할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 효율&lt;/b&gt; : static이 아니면 내부 클래스 객체는 자신을 감싸고 있는 외부 클래스의 정보를 항상 들고 있어야 한다. DTO처럼 단순 데이터를 담는 용도라면 이런 연결 고리는 메모리 낭비만 초래한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구조적 그룹화&lt;/b&gt; : 관련 있는 데이터 구조(Result, Score)를 따로 만들지 않고&lt;span style=&quot;color: #ef6f53;&quot;&gt; &lt;b&gt;하나의 파일 안에서 논리적으로 묶어 관리할 수 있어 가독성이 좋아진다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 static이 붙지 않는 내부 클래스는 부모(외부)가 있어야 태어날 수 있는 자식이고, static class는 부모 집 안에 살지만 자기 차와 열쇠를 따로 가지고 있는 독립된 성인이라 볼 수 있다.&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;(2) WebClient 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 RestTemplate를 이용해서 외부 API와 연결하는게 표준이었는데, 요즘 RestTemplate 쓰는것도 구시대적이라 권장되지 않는다고 한다.&lt;span style=&quot;color: #dddddd;&quot;&gt;&lt;s&gt; (그래서 선배가 리팩토링 하다가 기존 코드 다 이상해져서 롤백한건 안비밀)&lt;/s&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 초장부터 그냥 RestTemplate말고 좀 더 좋은게 뭐가 있을지 생각하다가 WebClient라는걸 발견했다. WebClient는 비동기 및 비차단 통신을 효율적으로 처리하는 반응형 Http client라고 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;비동기, 논블로킹(Non-Blocking) 방식 : WebClient는 Reactive Stack을 기반으로 한다. API요청을 보낸 뒤 응답이 올 때 까지 &lt;a href=&quot;https://adjh54.tistory.com/167&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스레드&lt;/a&gt;가 대기(Block)하지 않고 다른 일을 할 수 있다. 적은 수의 스레드로 훨씬 많은 동시 요청을 할 수 있기 때문에 서버의 처리량이 획기적으로 늘어난다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;싱글톤(Singleton)으로 관리되는 재사용성 : @Bean으로 등록하면 스프링 컨테이너가 WebClient 객체를 딱 하나만 만들어서 관리한다. 리소스를 절약하고 설정을 공유할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;유연한 확장성: 나중에 타임아웃 설정, 로깅용 필터, 특정 API 전용 인증 헤더 추가 등등을 builder에 한줄만 추가하면 된다. 또한 한번 빌드된 WebClient는 상태가 변하지 않으므로 여러 스레드에서 동시에 사용해도 안전하다.&lt;/li&gt;
&lt;li&gt;가독성과 유지보수 : .retrieve().bodyToMono(MatgimResponseDTO.class) 이거 한줄이면 API응답인 snake_case를 우리가 만든 camelCase로 자동 변환해준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;참고로 스레드(thread)란?&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjLVX6/dJMcaax3oG2/6njQjscAX3JR0ZpsgYEb41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjLVX6/dJMcaax3oG2/6njQjscAX3JR0ZpsgYEb41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjLVX6/dJMcaax3oG2/6njQjscAX3JR0ZpsgYEb41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjLVX6%2FdJMcaax3oG2%2F6njQjscAX3JR0ZpsgYEb41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;295&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;하나의 프로세스 안에서 독립적으로 실행되는 '작은 실행 단위'&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 비유하자면 &quot;일꾼&quot;이다.&amp;nbsp; 자바 애플리케이션(서버)이라는 식당에서 손님의 주문을 받고 요리해서 서빙까지 하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-path-to-node=&quot;3&quot; data-index-in-node=&quot;68&quot;&gt;직원 한 명&lt;/b&gt;이 곧 스레드 하나라고 생각하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-path-to-node=&quot;4&quot;&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;4,0,0&quot; data-index-in-node=&quot;0&quot;&gt;스레드가 많으면?&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;일꾼이 많으니 동시에 여러 손님을 응대할 수 있지만, 일꾼마다 월급(메모리 점유)을 줘야 하고 관리하기가 힘들어짐&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;4,1,0&quot; data-index-in-node=&quot;0&quot;&gt;스레드가 적으면?&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;서버가 사용하는 메모리가 적고 가볍지만, 일꾼이 모자라면 손님들이 줄을 서서 기다려야함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;827&quot; data-start=&quot;813&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ Thread 1개&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;요청1 &amp;rarr; 끝나야 &amp;rarr; 요청2 &amp;rarr; 끝나야 &amp;rarr; 요청3 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px; caret-color: auto;&quot;&gt;  느림&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;898&quot; data-start=&quot;882&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔ Thread 여러 개&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;요청1&lt;/span&gt;&lt;br /&gt;&lt;span&gt;요청2&lt;/span&gt;&lt;br /&gt;&lt;span&gt;요청3&lt;/span&gt;&lt;br /&gt;&lt;span&gt;동시에 처리&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px; caret-color: auto;&quot;&gt;  빠름&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&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;그럼 프로세스(process)란?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템에서 실행 중인 프로그램&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqKCEw/dJMcahKIpdM/nK97nENics9BTGbvHNVZhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqKCEw/dJMcahKIpdM/nK97nENics9BTGbvHNVZhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqKCEw/dJMcahKIpdM/nK97nENics9BTGbvHNVZhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqKCEw%2FdJMcahKIpdM%2FnK97nENics9BTGbvHNVZhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;601&quot; height=&quot;356&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;356&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 왜 최근에 RestTemplate가 아닌 WebClient가 사용되는지 이제 알겠다~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1️⃣ pom.xml에 의존성 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775200383956&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- webClient --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-webflux&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2️⃣ WebCientConfig 작성하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775200345093&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class WebClientConfig {

    @Bean
    public WebClient MatgimWebClient(){
       return WebClient.builder()
               .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
               .build();
    }
}&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;@Configuration : 스프링이 시작될 때 Configuration 어노테이션을 읽어서 설정을 실행함. 자바의 일반 클래스가 아닌 스프링 시스템의 설계도 역할&lt;/li&gt;
&lt;li&gt;@Bean : 다른 서비스 클래스에서 new WebClient()를 매번 할 필요 없이 스프링이 미리 만들어준 이 객체를 &quot;주입(@Autowired)&quot;바당 쓸 수 있음(싱글톤 패턴)&lt;/li&gt;
&lt;li&gt;public WebClient MatgimWebClient() : 생성될 객체의 타입은 WebClient, 이 Bean의 이름은 MatgimWebClient&lt;/li&gt;
&lt;li&gt;WebClient.builder() : WebClient 객체를 만들기 위한 조립 도구(Builder)를 꺼내는 단계&lt;/li&gt;
&lt;li&gt;defaultHeader(HttpHeader.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) : 클라이언트로 보내는 모든 요청은 기본적으로 JSON임을 명시함)&lt;/li&gt;
&lt;li&gt;build() : 설정을 마친 최종 객체가 생성되어 스프링 컨테이너에 보관됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3️⃣ API 호출하기&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;String body = request.getText();
Map&amp;lt;String,String&amp;gt; bodyMap = new HashMap&amp;lt;&amp;gt;();
bodyMap.put(&quot;document&quot;, body);

MatgimResponseDTO result = webClient.post()
        .uri(matgimUrl)
        .header(&quot;Content-Type&quot;, &quot;application/json&quot;)
        .header(&quot;x-auth-token&quot;, matgimApiKey)
        .bodyValue(bodyMap)
        .retrieve()
        .bodyToMono(MatgimResponseDTO.class)
        .block();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 service단에서 API 호출부를 작성하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 데이터 준비&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775202069933&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String body = request.getText(); // 사용자가 보낸 일기 내용 등을 가져옴
Map&amp;lt;String, String&amp;gt; bodyMap = new HashMap&amp;lt;&amp;gt;();
bodyMap.put(&quot;document&quot;, body); // API가 원하는 {&quot;document&quot;: &quot;내용&quot;} 형식으로 만듦&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호출부 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775202105895&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MatgimResponseDTO result = webClient.post() // 1. POST 방식
        .uri(matgimUrl) // 2. 목적지 주소(URL) 설정
        .header(&quot;Content-Type&quot;, &quot;application/json&quot;) // 3. JSON 데이터라고 알림
        .header(&quot;x-auth-token&quot;, matgimApiKey) // 4. 인증 키를 헤더에 담음
        .bodyValue(bodyMap) // 5. 준비한 데이터를 몸체(Body)에 실음
        .retrieve() // 6. 이제 요청을 쏨
        .bodyToMono(MatgimResponseDTO.class) // 7. 받은 JSON을 아까 만든 DTO 객체로 자동 변환
        .block(); // 8. 결과가 올 때까지 기다림 (동기적 처리)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;.block()의 의미:&lt;/b&gt; 원래 WebClient는 비동기(Non-blocking)로 동작하지만, 여기서 .block()을 쓰면 응답이 올 때까지 다음 코드로 넘어가지 않고 기다린다. 왜냐면&lt;/p&gt;
&lt;pre id=&quot;code_1775202196990&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;double score = result != null ? result.getResult().getDocumentScore().getScore() * 2.5 : 0;
        score = Math.max(-1, Math.min(1, score));

        double positive = result != null ? result.getResult().getDocumentScore().getPosScore() : 0;
        double negative = result != null ? result.getResult().getDocumentScore().getNegScore() : 0;
        double magnitude = (Math.abs(positive) + Math.abs(negative)) * 5;

        response.setScore(score);
        response.setMagnitude(magnitude);
        return ResponseEntity.ok(response);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 로직 때문에.. 기존에 google api에선 감정을 score과 magnitude로 분리해서 보내줬는데, 맡김은 긍정적인 감정, 부정적인 감정의 점수만 알려준다. 그래서 그걸 score과 magnitude로 변환한 개인적인 로직이다... &lt;span style=&quot;color: #ef5369;&quot;&gt;이 부분은 필수는 아니다!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZw9LV/dJMcaaY6axE/JKTS0Ea6TSe8HKBXKKKKi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZw9LV/dJMcaaY6axE/JKTS0Ea6TSe8HKBXKKKKi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZw9LV/dJMcaaY6axE/JKTS0Ea6TSe8HKBXKKKKi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZw9LV%2FdJMcaaY6axE%2FJKTS0Ea6TSe8HKBXKKKKi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;718&quot; height=&quot;300&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[참고 문헌]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/seek316/223337685249&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://m.blog.naver.com/seek316/223337685249&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1775198883714&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Java] Spring Boot - WebClient 사용하기&quot; data-og-description=&quot;WebClient란? WebClient는 Spring Boot 애플리케이션에서 HTTP 요청을 만드는 데 사용되는 강력...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://m.blog.naver.com/seek316/223337685249&quot; data-og-url=&quot;https://blog.naver.com/seek316/223337685249&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cH05IH/dJMb81fS29e/9I3vG7SJNcS8T6MbDcgRc0/img.jpg?width=452&amp;amp;height=314&amp;amp;face=0_0_452_314&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/seek316/223337685249&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://m.blog.naver.com/seek316/223337685249&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cH05IH/dJMb81fS29e/9I3vG7SJNcS8T6MbDcgRc0/img.jpg?width=452&amp;amp;height=314&amp;amp;face=0_0_452_314');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Java] Spring Boot - WebClient 사용하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;WebClient란? WebClient는 Spring Boot 애플리케이션에서 HTTP 요청을 만드는 데 사용되는 강력...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://coding-factory.tistory.com/279&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://coding-factory.tistory.com/279&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775199646105&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Java] 자바 Thread(스레드) 사용법 &amp;amp; 예제&quot; data-og-description=&quot;Thread란? 하나의 프로세스 내부에서 독립적으로 실행되는 하나의 작업 단위를 말하며, 세부적으로는 운영체제에 의해 관리되는 하나의 작업 혹은 태스크를 의미합니다. 스레드와 태스크(혹은 작&quot; data-og-host=&quot;coding-factory.tistory.com&quot; data-og-source-url=&quot;https://coding-factory.tistory.com/279&quot; data-og-url=&quot;https://coding-factory.tistory.com/279&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zGdTR/dJMb9jOnmLx/BwSbkAq3TjADzEVe4YIYNk/img.png?width=728&amp;amp;height=295&amp;amp;face=0_0_728_295,https://scrap.kakaocdn.net/dn/MDCtb/dJMb9cBIxJX/uTI5fMXHzHKO6cLinZavM1/img.png?width=728&amp;amp;height=295&amp;amp;face=0_0_728_295,https://scrap.kakaocdn.net/dn/T1NGv/dJMb9b3SukV/AIiD3TzG8DopxGCVcnMrQ1/img.png?width=728&amp;amp;height=295&amp;amp;face=0_0_728_295&quot;&gt;&lt;a href=&quot;https://coding-factory.tistory.com/279&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://coding-factory.tistory.com/279&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zGdTR/dJMb9jOnmLx/BwSbkAq3TjADzEVe4YIYNk/img.png?width=728&amp;amp;height=295&amp;amp;face=0_0_728_295,https://scrap.kakaocdn.net/dn/MDCtb/dJMb9cBIxJX/uTI5fMXHzHKO6cLinZavM1/img.png?width=728&amp;amp;height=295&amp;amp;face=0_0_728_295,https://scrap.kakaocdn.net/dn/T1NGv/dJMb9b3SukV/AIiD3TzG8DopxGCVcnMrQ1/img.png?width=728&amp;amp;height=295&amp;amp;face=0_0_728_295');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Java] 자바 Thread(스레드) 사용법 &amp;amp; 예제&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Thread란? 하나의 프로세스 내부에서 독립적으로 실행되는 하나의 작업 단위를 말하며, 세부적으로는 운영체제에 의해 관리되는 하나의 작업 혹은 태스크를 의미합니다. 스레드와 태스크(혹은 작&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;coding-factory.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;</description>
      <category>ToyProjects /Dalmuri⭐</category>
      <author>JanuDev</author>
      <guid isPermaLink="true">https://janudev.tistory.com/60</guid>
      <comments>https://janudev.tistory.com/60#entry60comment</comments>
      <pubDate>Fri, 3 Apr 2026 16:45:36 +0900</pubDate>
    </item>
    <item>
      <title>모르는 단어 정리_ WSL2, Ubuntu, WireGuard, VPN</title>
      <link>https://janudev.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Tailscale과 Nginx보다가 그냥 간단하게 정리하고 공부하고싶어서 쓴 글...&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. WSL2(Windows Subsystem for Linux2)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Window안에서 리눅스를 &quot;진짜처럼&quot; 실행할 수 있도록 하는 기능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Window PC 하나로 Windows + Linux 둘 다 쓸 수 있는 환경이다.&lt;/p&gt;
&lt;pre id=&quot;code_1774856724467&quot; class=&quot;cs&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[ Windows ]
     &amp;darr;
[ WSL2 (가벼운 가상머신 느낌) ]
     &amp;darr;
[ Linux (Ubuntu 등) ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 리눅스는 별도로 설치해야 하는데, WSL2는 Window안에서 리눅스를 바로 실행할 수 있도록 해준다.&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;WSL1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;WSL2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;방식&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;번역 방식&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;진짜 리눅스 커널 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;속도&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;느림&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;호환성&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;제한적&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;거의 완벽&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 현재는 무조건 WSL2를 사용하는 상태.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Linux 커널을 Window 안에서 실행하기 때문에&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker 실행 가능&lt;/li&gt;
&lt;li&gt;Linux 명령어 100% 실행 가능&lt;/li&gt;
&lt;li&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;서버 환경은 대부분 Linux이기 때문에 WSL2가 있으면 Mac이 없어도 Linux가 개발 가능하거나 배포 환경과 동일하게 테스트 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex)&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;Java + Spring 서버 실행&lt;/li&gt;
&lt;li&gt;Node.js 서버&lt;/li&gt;
&lt;li&gt;DB(PostgreSQL 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 WSL2안에서 돌린다.&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. Ubuntu&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(모른다기 보단 그냥 다시 한번 정리해서 짚고 넘어가는 정도)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스의 운영체제(OS)의 한 종류.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Window &amp;rarr; 윈도우 OS&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;macOS &amp;rarr; 애플 OS&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux &amp;rarr; 여러 종류가 있음. 그 중 하나가 Ubuntu인 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스는 오픈소스이기 때문에 여러 팀/회사들이 자기 스타일로 만든 버전들이 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. VPN(Virtual Private Network)&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;일반 인터넷&lt;/p&gt;
&lt;pre id=&quot;code_1774856688529&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;내 PC &amp;rarr; (공개 인터넷) &amp;rarr; 서버&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPN을 사용하면&lt;/p&gt;
&lt;pre id=&quot;code_1774856709346&quot; class=&quot;armasm&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;내 PC &amp;rarr; (암호화 터널) &amp;rarr; VPN 서버 &amp;rarr; 인터넷 &amp;rarr; 서버​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간 구간이 암호화된 터널로 보호된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 (1) 보안 문제, (2) IP 숨김(내 실제 IP대신 VPN서버 IP 사용), (3) 네트워크 우회(지역제한 서비스 접근)를 목적으로 사용한다.&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;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;/li&gt;
&lt;li&gt;해외에서 한국 사이트 접속&lt;/li&gt;
&lt;li&gt;카페 Wifi에서 안전하게 로그인&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. WireGuard&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPN을 구현하는 &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;그러니까 VPN = 개념&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WireGuard = 그걸 만드는 기술 중 하나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 VPN 방식이 openVPN, IPSec 등이 있었는데, 이것들의 문제점은... 설정이 복잡하고 속도가 느리고 코드가 무겁다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 WireGuard는 (1) 매우 빠르고(최신 암호화 알고리즘 사용), (2) 코드가 간단하고(약 4000줄), (3) 설정이 쉽다(key 기반 구조)&lt;/p&gt;
&lt;pre id=&quot;code_1774857022057&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Client (Private Key)
   &amp;darr;
[ 암호화 ]
   &amp;darr;
Server (Public Key로 검증)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH(전하지 않은 네트워크를 통해 원격 컴퓨터에 접속하여 명령어를 실행하거나 파일을 전송할 때, 키 기반 인증(SSH Key)의 암호화 기술을 사용하여 데이터를 보호하는 네트워크 프로토콜)랑 비슷한 구조.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WeKBk/dJMcahcQMvK/337chRr0XeykhiNI8vayI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WeKBk/dJMcahcQMvK/337chRr0XeykhiNI8vayI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WeKBk/dJMcahcQMvK/337chRr0XeykhiNI8vayI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWeKBk%2FdJMcahcQMvK%2F337chRr0XeykhiNI8vayI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;718&quot; height=&quot;299&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Server, Deploy /Server </category>
      <category>linux</category>
      <category>mac os</category>
      <category>ubuntu</category>
      <category>VPN</category>
      <category>Window OS</category>
      <category>wireguard</category>
      <category>WSL2</category>
      <category>운영체제</category>
      <author>JanuDev</author>
      <guid isPermaLink="true">https://janudev.tistory.com/59</guid>
      <comments>https://janudev.tistory.com/59#entry59comment</comments>
      <pubDate>Mon, 30 Mar 2026 16:53:07 +0900</pubDate>
    </item>
    <item>
      <title>라즈베리파이4 서버 구축기 - (4) 외부에서 내 프로젝트 접속하기</title>
      <link>https://janudev.tistory.com/58</link>
      <description>&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; (4) 외부 접속 설정 (선택)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 공유기 포트포워딩 설정 예) 80 포트를 라즈베리파이의 80 포트로 포워딩&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. DDNS 설정 (선택): 외부에서 접속할 수 있도록 도메인 주소 할당 (No-IP, DuckDNS 등 사용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. SSL 적용 (선택): certbot으로 Let's Encrypt 인증서 설정하면 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;....뭐 이런식으로 썼었는데, 이것도 AI한테 물어봐서 정보를 얻은거라 확실하진 않아서 내가 직접 찾아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Let's Encrypt 인증서란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료로 HTTPS 보안 연결을 가능하게 해주는 인증서 발급기관.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷에선 보안을 위해 데이터를 암호화 해주는 HTTPS 인증서를 사용해야 한다(이건 기존의 나도 알고 있던것). HTTPS를 쓰면 데이터가 암호화되고, 로그인 정보 및 쿠키가 보호된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹페이지가 HTTPS가 되려면 서버에 인증서가 있어야 하는데, 이 인증서는 (1) 이 서버가 진짜임을 증명 (2) 암호화 통신 시작을 위한 키 제공 등의 역할을 하고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Let's Encrypt : 인증서를 발급해주는 기관(CA), 실제 인증서를 만들어서 내려줌&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://certbot.eff.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Certbot&lt;/a&gt; : 인증서를 자동으로 받아오는 프로그램. Let's Encrypt에 요청 보내고 인증서 받아서 서버 설치하고 만료되면 자동 갱신까지 해주고....&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가 Tailscale를 알게됬다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Tailscale이란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WireGuard 프로토콜을 기반으로 한 현대적인 매쉬 VPN솔루션으로, 복잡한 네트워크 설정없이 장치 간 안전하고 빠른 연결을 제공한다. 뭔 말인지 이해 안간다면, &quot;집 밖에서도 집 안에 있는것처럼 접속하게 만들어주는 도구&quot;라고 설명할 수 있을 것이다.&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;pre id=&quot;code_1774851192089&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;다른 장소 &amp;rarr; 인터넷 &amp;rarr; 공유기(포트포워드) &amp;rarr; 라즈베리파이(홈서버)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 방법을 이용하기 때문에 외부 접속 시 포트포워드 설정, 공인IP 및 DDNS 설정, 보안 등의 번거로움이 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Talescale은 공유기, 포트포워딩 등의 선행 작업이 모두 무시된다.&lt;/p&gt;
&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;1️⃣ 가짜 내부 네트워크를 만듦&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집이든 회사든 어느 장소든 상관 없이 모든 기기를 같은 네트워크에 있는 것처럼 만들어주는데, 그래서 홈서버에는 Tailscale 전용 IP가 생긴다.&lt;/p&gt;
&lt;pre id=&quot;code_1774852091865&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;100.x.x.x (Tailscale 전용 IP)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2️⃣ 그 주소로 바로 접속 가능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널(리눅스, 윈도우 powershell, 맥)에서 다음과 같이 작성하면 열린다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774852724640&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh pi@100.x.x.x&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;단 저렇게 하면 당연히 일반 사용자들은 들어올 수 없기 때문에.... 난 Tailscale Funnel 무료 버전을 사용하기로 했다.&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;1. 라즈베리파이에 웹서버 실행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm run dev, http://localhost:3000 등으로 라즈베리파이 네부에서 접속이 되어 있어야 한다.&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;2. Tailscale 설치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라즈베리파이 os에서 curl명령어를 쓸 수 있으므로&lt;/p&gt;
&lt;pre id=&quot;code_1774853877539&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -fsSL https://tailscale.com/install.sh | sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 작성하고, 설치 후&lt;/p&gt;
&lt;pre id=&quot;code_1774853923198&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo tailscale up&lt;/code&gt;&lt;/pre&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;3. Funnel 활성화&lt;/h4&gt;
&lt;pre id=&quot;code_1774853996413&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo tailscale funnel 3000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라즈베리파이의 3000번 포트를 외부에 공개하겠다는 뜻으로, 각 포트에 맞게 작성하면 된다.&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;4. URL 생성 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령을 실행하면&lt;/p&gt;
&lt;pre id=&quot;code_1774854045852&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;https://xxxx.ts.net&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 나오는데, 이게 바로 외부에서 접속 가능한 URL이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 나의 경우 Vite + Typescript + React를 사용한 web단과 Spring Boot를 사용한 WAS단이 따로 나뉘어져 있기 때문에 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;리버스 프록시(Nginx)를 사용해야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;pre id=&quot;code_1774854833966&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;외부 (https://xxx.ts.net)
        &amp;darr;
     nginx
   ├─ /        &amp;rarr; React (정적 파일)
   └─ /api     &amp;rarr; Spring Boot (8080)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전체 단계는 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(1) React 빌드&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(2) NGINX 설치&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(3) NGINX 설정&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(4) Spring Boot 실행&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(5) Tailscale Funnel 연결&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. React 빌드&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라즈베리 파이에서 react 프로젝트를 빌드한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774854866490&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같을거다.&lt;/p&gt;
&lt;pre id=&quot;code_1774854880913&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dist/
 ├─ index.html
 ├─ assets/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. nginx 설치&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1774854902820&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install nginx -y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 확인은&lt;/p&gt;
&lt;pre id=&quot;code_1774855167108&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nginx -v&lt;/code&gt;&lt;/pre&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. nginx 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일 열기&lt;/p&gt;
&lt;pre id=&quot;code_1774855192676&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo nano /etc/nginx/sites-available/default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;641&quot; data-start=&quot;619&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;기존 내용 다 지우고 아래로 교체한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774855228954&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    listen 80;
    server_name _;

    # React (프론트)
    root /home/pi/project/dist;
    index index.html;

    location / {
        try_files $uri /index.html;
    }

    # Spring Boot (백엔드)
    location /api/ {
        proxy_pass http://localhost:8080/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요 포인트는, root경로를 내 dist 폴더 위치로 바꾸는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1774855259124&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;root /home/pi/project/dist;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. nginx 재시작&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1774855302001&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo systemctl restart nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. Spring Boot 실행&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;maven 빌드 명령어를 실행해서 jar 파일을 만드는 것이 핵심이다.&lt;/p&gt;
&lt;pre id=&quot;code_1774855343506&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mvn clean package&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;clean : 이전에 빌드했던 찌꺼기 파일을 지움&lt;/li&gt;
&lt;li&gt;package: 소스 코드를 컴파일하고 테스트를 걸쳐 target폴더 안에 .jar파일을 생성함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공 시 target/ 폴더 안에 프로젝트명-0.0.1-SNAPSHOT.jar과 같은 파일이 생성된다. 그리고 이 파일을 라즈베리파이 컴퓨터로 보내야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774855518380&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 형식: scp [파일경로] [사용자]@[라즈베리파이IP]:[복사할경로]
scp target/*.jar pi@raspberrypi.local:/home/pi/project/app.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어 작성 시 내 pc에 있던 jar파 일이 라즈베리파이의 /home/pi/project/폴더로 app.jar라는 이름으로 복사된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. Tailscale Funnel 연결&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1774855662415&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo tailscale funnel 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[참고 자료]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@zvyg1023/Certbot-ssl-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EB%B0%9C%EA%B8%89%EB%B0%9B%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@zvyg1023/Certbot-ssl-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EB%B0%9C%EA%B8%89%EB%B0%9B%EA%B8%B0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774847264115&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;Let's Encrypt - Certbot으로 ssl 인증서 발급받기&quot; data-og-description=&quot;이제 본격적인 https 세팅을 시작해볼 것이다. Let's Encrypt 라는 비영리 기관을 통해 무료로 SSL 인증서를 발급받을 수 있다. 인증서 발급을 위해 Certbot을 사용해보자. 1. certbot 설치하기 Certbot 설치는&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@zvyg1023/Certbot-ssl-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EB%B0%9C%EA%B8%89%EB%B0%9B%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@zvyg1023/Certbot-ssl-인증서-발급받기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/di7NJv/dJMb8UHPaPF/TGyhOUu4vZXXmSZEg2Ehq0/img.png?width=911&amp;amp;height=281&amp;amp;face=0_0_911_281,https://scrap.kakaocdn.net/dn/cYv34u/dJMb8SXxN1M/nb60Gb0hyAMGba5eNPvhYk/img.png?width=911&amp;amp;height=281&amp;amp;face=0_0_911_281,https://scrap.kakaocdn.net/dn/bK18Uk/dJMb8UHPaPG/dBeB9ZwHNsxrYtzretZKE1/img.png?width=1024&amp;amp;height=1536&amp;amp;face=0_0_1024_1536&quot;&gt;&lt;a href=&quot;https://velog.io/@zvyg1023/Certbot-ssl-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EB%B0%9C%EA%B8%89%EB%B0%9B%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@zvyg1023/Certbot-ssl-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EB%B0%9C%EA%B8%89%EB%B0%9B%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/di7NJv/dJMb8UHPaPF/TGyhOUu4vZXXmSZEg2Ehq0/img.png?width=911&amp;amp;height=281&amp;amp;face=0_0_911_281,https://scrap.kakaocdn.net/dn/cYv34u/dJMb8SXxN1M/nb60Gb0hyAMGba5eNPvhYk/img.png?width=911&amp;amp;height=281&amp;amp;face=0_0_911_281,https://scrap.kakaocdn.net/dn/bK18Uk/dJMb8UHPaPG/dBeB9ZwHNsxrYtzretZKE1/img.png?width=1024&amp;amp;height=1536&amp;amp;face=0_0_1024_1536');&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;Let's Encrypt - Certbot으로 ssl 인증서 발급받기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이제 본격적인 https 세팅을 시작해볼 것이다. Let's Encrypt 라는 비영리 기관을 통해 무료로 SSL 인증서를 발급받을 수 있다. 인증서 발급을 위해 Certbot을 사용해보자. 1. certbot 설치하기 Certbot 설치는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devopslog.tistory.com/169&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devopslog.tistory.com/169&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774849669841&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;Tailscale 가이드 - 안전한 매쉬 VPN 활용법&quot; data-og-description=&quot;Tailscale의 핵심 개념, VPN 정의, 동작 방식, 설치 및 구성 방법, 엔드포인트 관리, 포트 제어, 다른 단말 접속, 그리고 보안 설정 방법Tailscale이란?Tailscale은 WireGuard 프로토콜을 기반으로 한 현대적인&quot; data-og-host=&quot;devopslog.tistory.com&quot; data-og-source-url=&quot;https://devopslog.tistory.com/169&quot; data-og-url=&quot;https://devopslog.tistory.com/169&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dzoS2R/dJMb82eM3qt/7gpqPyi0pA5TR2TSqdFtKk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/xYdJK/dJMb9jgxgMj/oMg3n35Y2LBmkNlpItBYgk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://devopslog.tistory.com/169&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devopslog.tistory.com/169&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dzoS2R/dJMb82eM3qt/7gpqPyi0pA5TR2TSqdFtKk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/xYdJK/dJMb9jgxgMj/oMg3n35Y2LBmkNlpItBYgk/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;Tailscale 가이드 - 안전한 매쉬 VPN 활용법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Tailscale의 핵심 개념, VPN 정의, 동작 방식, 설치 및 구성 방법, 엔드포인트 관리, 포트 제어, 다른 단말 접속, 그리고 보안 설정 방법Tailscale이란?Tailscale은 WireGuard 프로토콜을 기반으로 한 현대적인&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devopslog.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=vCM-demQ4MY&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=vCM-demQ4MY&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=vCM-demQ4MY&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/ck5QpT/dJMb8U8TCyn/3fzLlTyrBO3bgEDiq42Jxk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/bRbOPh/dJMb82eM3Qd/y5vwhmsiGwAJoFy0snrFK0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/bUG3VC/dJMb8868P3q/ne5Tyo9vNXtIAMlpwyXCl0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;22.Tailscale 원격 접근 완벽 가이드&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/vCM-demQ4MY&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/jacking75/NewbieGameServerProgrammerLearningMaterials/blob/main/Tailscale.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/jacking75/NewbieGameServerProgrammerLearningMaterials/blob/main/Tailscale.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774853019805&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;NewbieGameServerProgrammerLearningMaterials/Tailscale.md at main &amp;middot; jacking75/NewbieGameServerProgrammerLearningMaterials&quot; data-og-description=&quot;뉴비 게임 서버 프로그래머를 위한 학습 자료. Contribute to jacking75/NewbieGameServerProgrammerLearningMaterials development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jacking75/NewbieGameServerProgrammerLearningMaterials/blob/main/Tailscale.md&quot; data-og-url=&quot;https://github.com/jacking75/NewbieGameServerProgrammerLearningMaterials/blob/main/Tailscale.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cgQC7U/dJMb84XYL2A/kgzAu6VPGg7jlNL1VDfBXk/img.png?width=1200&amp;amp;height=600&amp;amp;face=988_133_1068_221,https://scrap.kakaocdn.net/dn/b1R10D/dJMb85WTmzv/pxGbkluHB4WcR72mK7HkF1/img.png?width=1200&amp;amp;height=600&amp;amp;face=988_133_1068_221&quot;&gt;&lt;a href=&quot;https://github.com/jacking75/NewbieGameServerProgrammerLearningMaterials/blob/main/Tailscale.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jacking75/NewbieGameServerProgrammerLearningMaterials/blob/main/Tailscale.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cgQC7U/dJMb84XYL2A/kgzAu6VPGg7jlNL1VDfBXk/img.png?width=1200&amp;amp;height=600&amp;amp;face=988_133_1068_221,https://scrap.kakaocdn.net/dn/b1R10D/dJMb85WTmzv/pxGbkluHB4WcR72mK7HkF1/img.png?width=1200&amp;amp;height=600&amp;amp;face=988_133_1068_221');&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;NewbieGameServerProgrammerLearningMaterials/Tailscale.md at main &amp;middot; jacking75/NewbieGameServerProgrammerLearningMaterials&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;뉴비 게임 서버 프로그래머를 위한 학습 자료. Contribute to jacking75/NewbieGameServerProgrammerLearningMaterials development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cwpack0730.tistory.com/117&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://cwpack0730.tistory.com/117&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774854351652&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;[NGINX] nginx reverse proxy&quot; data-og-description=&quot;1. 개요현재 매 월마다 서비스 정기점검을 진행하고 있다.&amp;nbsp;보통 새벽시간대에 점검을 진행하는데 이 때 일반 사용자는 점검중인 서비스에 접근할 수 없도록 점검 페이지로 이동해야 하며, 개발&quot; data-og-host=&quot;cwpack0730.tistory.com&quot; data-og-source-url=&quot;https://cwpack0730.tistory.com/117&quot; data-og-url=&quot;https://cwpack0730.tistory.com/117&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cR2HiP/dJMb8Xkfn4k/a7SzKbVSqzKmQRUBk2foKK/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/f9nhy/dJMb8UHPbDu/imzRSTLcvz7t1ZMvh8MTA1/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/jYDNY/dJMb9efdZE9/A47gR2vPNN6M6PgKI1phFK/img.png?width=3000&amp;amp;height=1805&amp;amp;face=0_0_3000_1805&quot;&gt;&lt;a href=&quot;https://cwpack0730.tistory.com/117&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cwpack0730.tistory.com/117&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cR2HiP/dJMb8Xkfn4k/a7SzKbVSqzKmQRUBk2foKK/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/f9nhy/dJMb8UHPbDu/imzRSTLcvz7t1ZMvh8MTA1/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/jYDNY/dJMb9efdZE9/A47gR2vPNN6M6PgKI1phFK/img.png?width=3000&amp;amp;height=1805&amp;amp;face=0_0_3000_1805');&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;[NGINX] nginx reverse proxy&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 개요현재 매 월마다 서비스 정기점검을 진행하고 있다.&amp;nbsp;보통 새벽시간대에 점검을 진행하는데 이 때 일반 사용자는 점검중인 서비스에 접근할 수 없도록 점검 페이지로 이동해야 하며, 개발&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cwpack0730.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTMBXY/dJMcaax2WvK/8drKYY62W0TthwZxZwGQEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTMBXY/dJMcaax2WvK/8drKYY62W0TthwZxZwGQEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTMBXY/dJMcaax2WvK/8drKYY62W0TthwZxZwGQEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTMBXY%2FdJMcaax2WvK%2F8drKYY62W0TthwZxZwGQEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;716&quot; height=&quot;299&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Server, Deploy /Server </category>
      <category>nginx</category>
      <category>raspberry pi</category>
      <category>Reverse Proxy</category>
      <category>server</category>
      <category>tailscale</category>
      <category>Tailscale Funnel</category>
      <category>라즈베리파이</category>
      <category>배포</category>
      <category>운영</category>
      <author>JanuDev</author>
      <guid isPermaLink="true">https://janudev.tistory.com/58</guid>
      <comments>https://janudev.tistory.com/58#entry58comment</comments>
      <pubDate>Mon, 30 Mar 2026 16:36:08 +0900</pubDate>
    </item>
    <item>
      <title>람다식이란?</title>
      <link>https://janudev.tistory.com/57</link>
      <description>&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;pre id=&quot;code_1769042550359&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UserDTO user = jdbcTemplate.queryForObject(
    sql,
    new Object[]{userId},
    (rs, rowNum) -&amp;gt; {   // 람다
        UserDTO u = new UserDTO();
        u.setUserCd(rs.getString(&quot;USER_CD&quot;));
        u.setUserId(rs.getString(&quot;USER_ID&quot;));
        u.setUserNm(rs.getString(&quot;USER_NM&quot;));
        u.setPassword(rs.getString(&quot;PASSWORD&quot;));
        u.setBgColor(rs.getString(&quot;BG_COLOR&quot;));
        u.setTextColor(rs.getString(&quot;TEXT_COLOR&quot;));
        u.setInsertDatetime(rs.getString(&quot;INSERT_DATETIME&quot;));
        return u;
    }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;() -&amp;gt; {} 이런 형태는 처음 보는거 같다. 뭔가 낮설다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 람다식이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;익명함수의 일종으로, 함수를 이름 없이 짧게 쓰는 문법(자바 8부터 사용 가능)&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. 람다식의 문법&lt;/h3&gt;
&lt;div data-path-to-node=&quot;15&quot;&gt;
&lt;div data-math=&quot;(파라미터) -&amp;gt; { 실행문 }&quot;&gt;$$(파라미터) -&amp;gt; { 실행문 }$$&lt;/div&gt;
&lt;/div&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;파라미터(rs, rowNum) : 메소드에서 전달받는 인자. 타입은 컴파일러가 알아서 추론하므로 생략 가능.&lt;/li&gt;
&lt;li&gt;화살표(-&amp;gt;) : 뒤 실행문 {} 을 실행한다는 의미&lt;/li&gt;
&lt;li&gt;실행문({}) : 실제로 수행할 코드 블럭. 실행문이 한줄이면 자바스크립트처럼 return으로 생략도 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 람다식 사용 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 생각보다 사용할 수 있는 상황이 그렇게 많진 않은 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;   &lt;b&gt;함수형 인터페이스여야 한다&lt;/b&gt; &lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;함수형 인터페이스란? 메소드가 딱 1개만 있는 &lt;b&gt;인터페이스&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769654435513&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface MyFunc {
    void doSomething();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저렇게 메소드가 1개인 인터페이스는 괜찮은데, 2개 이상인 경우 어느것을 실행해야 하는지 몰라서 멈추게 된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;인터페이스&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;메서드&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Runnable&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;run()&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;실행만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Comparator&amp;lt;T&amp;gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;compare()&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;비교&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Predictate&amp;lt;T&amp;gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;test()&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;조건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Function&amp;lt;T, R&amp;gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;apply()&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Consumer&amp;lt;T&amp;gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;accept()&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;사용만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Supplier&amp;lt;T&amp;gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;get()&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;제공&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;pre id=&quot;code_1769654650353&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface B {
    void a();
    void b();   // ❌
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1769654691055&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Test {
    int calc(int x, int y);
}

(x, y) -&amp;gt; x + y ✔
(x) -&amp;gt; x ❌&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 람다식의 장단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 간결해짐 : 익명클래스를 만들 때 필요한 반복적인 코드가 사라진다.&lt;/li&gt;
&lt;li&gt;가독성 향상 : 불필요한 선언부가 줄기 때문에 코드가 명확해진다.&lt;/li&gt;
&lt;li&gt;병렬 처리 유리 : 람다를 Stream API와 함께 사용하면 데이터 컬렉션을 훨씬 쉽고 빠르게(병렬로)처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ 단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디버깅 어려움: StackTrace에서 찾기 어렵다,&lt;/li&gt;
&lt;li&gt;재사용 불가능: 똑같은 로직이어도 다른곳에서 필요하면 복사하거나 별도의 메소드 분리가 필요하다.&lt;/li&gt;
&lt;li&gt;람다 남용 시 가독성 저해&lt;/li&gt;
&lt;li&gt;재귀 호출에 부적합&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EHgCK/dJMcaaRF2hU/JtTdsUl0UdJv6xbizG6eT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EHgCK/dJMcaaRF2hU/JtTdsUl0UdJv6xbizG6eT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EHgCK/dJMcaaRF2hU/JtTdsUl0UdJv6xbizG6eT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEHgCK%2FdJMcaaRF2hU%2FJtTdsUl0UdJv6xbizG6eT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;300&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Backend ️/Java☕(Spring )</category>
      <category>java</category>
      <category>java 8</category>
      <category>람다</category>
      <category>람다식</category>
      <category>자바</category>
      <author>JanuDev</author>
      <guid isPermaLink="true">https://janudev.tistory.com/57</guid>
      <comments>https://janudev.tistory.com/57#entry57comment</comments>
      <pubDate>Thu, 29 Jan 2026 11:47:51 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] JWT 이용해서 로그인 기능 만들기 - 필터 작성</title>
      <link>https://janudev.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;공부용이기 때문에 복잡한 로직은 넣지 않고 최대한 단축해서 작성하려 했는데 내용이 너무 길다..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직전까지 JWT토큰을 이용해서 로그인하는것 까진 성공했으니, 이젠 클라이언트 요청으로부터 토큰값을 받아 controller로 요청을 보내는 JWT Filter 기능을 적용해볼거다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 filter을 적용하지 않았을 땐 무조건 403 forbidden에러가 발생했는데, 왜냐면 사용자의 신원을 확인하는 로직(= SecurityContext에 Authentication을 아직 구현 안함)이 없어 무조건 권한이 없다는 return값을 출력하도록 냅뒀기 때문이다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이제부터 Filter 적용기를 써내려본다...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3JwQd/dJMcahwkJJQ/c3m1rtVQK9gLFhkNTDz0kk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3JwQd/dJMcahwkJJQ/c3m1rtVQK9gLFhkNTDz0kk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3JwQd/dJMcahwkJJQ/c3m1rtVQK9gLFhkNTDz0kk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3JwQd%2FdJMcahwkJJQ%2Fc3m1rtVQK9gLFhkNTDz0kk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;263&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/p&gt;
&lt;pre id=&quot;code_1767753790819&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;쿨라이언트 요청

&amp;rarr; SecurityFilterChain

&amp;rarr; 여러 Spring Security 필터들

&amp;rarr; JwtAuthenticationFilter

&amp;rarr; FilterSecurityInterceptor (authorizeHttpRequests 설정을 기반으로 권한 검사)

&amp;rarr; 컨트롤러&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 거쳐서 Controller로 진입한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 SecurityConfig는 &lt;b&gt;필터들의 순서표&lt;/b&gt;를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. SecurityFilterChain&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 만들어둔 filterChain이 있었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767754169452&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterhain filterChain (HttpSecurity http, JwtProvider jwtProvider) throws Exception {
  http
    // 허용 범위(권한 설정)
    .authorizeHttpRequests(auth -&amp;gt; auth
            .requestMatchers(HttpMethod.POST, &quot;/login&quot;).permitAll()     // 로그인은 허용
            .anyRequest().authenticated()                               // 그 외는 인증 필요
    )

    // CSRF 및 CORS 설정
    .csrf(csrf -&amp;gt; csrf.disable())      // REST API이므로 CSRF 끄기
    .cors(Customizer.withDefaults())   // CORS 설정 허용

    // 헤더 설정
    .headers(headers -&amp;gt; headers.frameOptions(frameOptions -&amp;gt; frameOptions.disable()))
    
    //////// 추가! //////////
    // 세션 설정(jwt 필수 설정)
    .sessionManagement(session -&amp;gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

    // jwt필터 설정
    .addFilterBefore(new JwtAuthenticationFilter(jwtProvider),
      UsernamePasswordAuthenticationFilter.class);


  return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 여기서 세션 설정과 jwt 필터 설정을 추가해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 세션설정&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 98px;&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: 22px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 22px; text-align: center;&quot;&gt;전통적인 로그인 방식&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 22px; text-align: center;&quot;&gt;JWT 로그인 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 22px;&quot;&gt;아이디, 비번 로그인&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 22px;&quot;&gt;로그인 성공시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 22px;&quot;&gt;서버가 세션(session)을 생성&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 22px;&quot;&gt;서버가 JWT 토큰을 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;서버 메모리에 '이 사람 로그인됨'을 저장&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;서버는 아무것도 하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;브라우저는 세션 ID를 쿠키로 들고 다님&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;클라이언트가 매 요청마다 토큰을 들고옴&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 로그인 방식은 아이디, 비밀번호 입력 후 로그인 시 서버에서 세션Session을 생성하는데, JWT방식에선 세션은 아무 일도 하지 않고 클라이언트로부터 넘어온 토큰값을 인증해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 세션 설정을 끄지 않으면&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Security가 몰래 세션을 만들 수 있다&lt;/li&gt;
&lt;li&gt;JWT + 세션이 섞인다&lt;/li&gt;
&lt;li&gt;로그아웃, 만료, 인증 흐림이 꼬인다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 JWT를 쓴다면 세션은 STATELESS로 놔두어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767764593591&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.sessionManagement(session -&amp;gt; session.sessionCreationPoplicy(SessionCreationPolicy.STATELESS))&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;sessionManagement() : 세션 관리 기능 설정의 시작점&lt;/li&gt;
&lt;li&gt;session.sessionCreationPolicy : 서버가 세션을 언제 생성할지 설정&lt;/li&gt;
&lt;li&gt;SessionCreationPolicy.STATELESS : 세션 생성 및 사용 비활성화(주로 JWT 로그인 시 사용됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) JWT 필터 설정&lt;/h4&gt;
&lt;pre id=&quot;code_1767764791927&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.addFilterBefore(new JwtAuthenticationFilter(jwtProvider), 
  UsernamePasswordAuthenticationFilter.class);&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;addFilterBefore(A, B) : B필터가 실행되기 바로 직전에 A필터 먼저 실행&lt;/li&gt;
&lt;li&gt;JwtAuthenticationFilter(jwtProvider) : 토큰 인증 검사 - 사용자가 보낸 JWT 토큰이 유효한지 검사하는 커스텀 필터&lt;/li&gt;
&lt;li&gt;UsernamePasswordAuthenticationFilter : Spring Security의 기본 필터, 아이디와 비밀번호를 로그인 시 확인하는 필터&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. JwtProvider&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JwtAuthenticationFilter를 거칠 때 JwtProvider를 건네야 한다. 왜냐면 JwtAuthenticationFilter에서 토큰에 대한 유효성 검사와 사용자 정보 추출 및 인증 객체 생성이 이루어지기 때문인데, 그 메소드가 JwtProvider에 있기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767770560603&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class JwtProvider {

    private final UserDetailsServiceImpl userDetailsServiceImpl;

    private Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    public JwtProvider(UserDetailsServiceImpl userDetailsServiceImpl) {
        this.userDetailsServiceImpl = userDetailsServiceImpl;
    }

    /**
     * JWT 토큰 생성
     * */
    public String createAccessToken (Authentication authentication) {

        UserDetailsDTO principal = (UserDetailsDTO) authentication.getPrincipal();
        String userId = principal.getUserId();
        List&amp;lt;String&amp;gt; roles = principal.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();

        Date now = new Date();
        Date expiry = new Date(now.getTime() + ACCESS_TOKEN);

        return Jwts.builder()
                .setSubject(userId)
                .claim(&quot;role&quot;, roles)
                .setIssuedAt(now)
                .setExpiration(expiry)
                .signWith(secretKey, SignatureAlgorithm.HS256)
                .compact();

    }

    /**
     * 토큰 유효성 검사
     * */
    public boolean validateToken (String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(secretKey)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false; // 만료되었거나 서명이 틀린 경우
        }
    }

    /**
     * 사용자 정보 추출 및 인증 객체 생성
     * */
    public Authentication getAuthentication (String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody();
        UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(claims.getSubject());
        return new UsernamePasswordAuthenticationToken(userDetails, &quot;&quot;, userDetails.getAuthorities());
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰이&amp;nbsp;만료되었거나,&amp;nbsp;서명이&amp;nbsp;일치하지&amp;nbsp;않거나,&amp;nbsp;형식이&amp;nbsp;잘못된&amp;nbsp;경우&amp;nbsp;모두&amp;nbsp;false를&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;지금 필터링 과정에서 필요한건 validationToken메소드와 getAuthentication 메소드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JwtProvider는 JWT의 생명주기(생성, 검증, 정보추출)를 관리하는 클리스이다.&lt;/p&gt;
&lt;pre id=&quot;code_1767770785361&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final UserDetailsServiceImpl userDetailsServiceImpl;

private Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);

public JwtProvider(UserDetailsServiceImpl userDetailsServiceImpl) {
    this.userDetailsServiceImpl = userDetailsServiceImpl;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JwtProvider에 UserDetailsService가 필요한 이유는&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰을 생성할 때&lt;/li&gt;
&lt;li&gt;사용자를 검증하고 인증객체를 반환할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 디테일한 전체 정보(사용자가 활성화 상태인지, 계정이 잠겼는지, 권한이 있는지 등)을 UserDetailsService에서 가져올 수 있기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 토큰 유효성 검사&lt;/h4&gt;
&lt;pre id=&quot;code_1767771603961&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; public boolean validateToken (String token) {
    try {
        Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token);
        return true;
    } catch (Exception e) {
        return false; // 만료되었거나 서명이 틀린 경우
    }
}&lt;/code&gt;&lt;/pre&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;setSigningKey(secretKey) : 우리가 만든 secretKey를 끼워 넣음&lt;/li&gt;
&lt;li&gt;build()&amp;nbsp;: JWT 파서(Parser) 생성&lt;/li&gt;
&lt;li&gt;parseClaimsJws(token) : 토큰을 열어서 해석 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해석이 성공하면 유효한 토큰 = 즉 true를 내뱉고, 그렇지 않은 경우 false를 내뱉는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 점은 서버가 가지고 있는 secretKey와 토큰 뒤에 붙은 서명이 일치하는지를 확인하여 위변조를 방지하는 것.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 인증 정보 추출(사용자 검증)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰이 유효하다면, 토큰 안에 적힌 사용자 ID(Subject)를 보고 DB에서 실제 정보를 가져와 시큐리티용 신분증을 만든다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767771809511&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Authentication getAuthentication (String token) {
    Claims claims = Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(token)
            .getBody();
    UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(claims.getSubject());
    return new UsernamePasswordAuthenticationToken(userDetails, &quot;&quot;, userDetails.getAuthorities());
}&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;Claims : 토큰 내부의 데이터(Claims)를 꺼냄&lt;/li&gt;
&lt;li&gt;userDetaisServiceImple.loadUserByUsername(claims.getSubject()) : 토큰 주인(Subject)의 ID로 DB에서 사용자 정보를 로드함&lt;/li&gt;
&lt;li&gt;new UsernamePasswordAuthenticationToken() : 스프링 시큐리티 내부에서 사용할 인증 객체(=신분증)를 생성하여 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. JwtAuthenticationFilter&lt;/p&gt;
&lt;pre id=&quot;code_1767772021743&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;

    /**
     * JWT 인증 Filter 처리 로직
     */
    @Override
    protected void doFilterInternal (
        @NonNull HttpServletRequest request,
        @NonNull HttpServletResponse response,
        @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        
        // 1. 요청 헤더에서 토큰 추출
        String accessToken = resolveToken(request);
        
        // 2. 토큰이 유효한지 검증
        if (accessToken != null &amp;amp;&amp;amp; jwtProvider.validateToken(accessToken)) {
            // 토큰이 유효하다면 인증 객체를 가져와서 SecurityContext에 담음
            Authentication auth = jwtProvider.getAuthentication(accessToken);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        
        // 3. 다음 필터로 넘김
        filterChain.doFilter(request, response);
    }

    // 헤더에서 &quot;Bearer&quot;를 제외한 실제 토큰만 꺼내는 메소드
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(&quot;Authorization&quot;);
        if (StringUtils.hasText(bearerToken) &amp;amp;&amp;amp; bearerToken.startsWith(&quot;Bearer&quot;)) {
            return bearerToken.substring(7);
        }
        return null;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resolveToken은 실제 토큰값을 꺼내는 메소드고, 진짜 핵심은 doFilterInternal부분이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OncePerRequestFilter 상속 : 사용자의 요청 한 번당 딱 한번만 실행되는 필터 라는 뜻. 요청이 내부적으로 다른 서블릿으로 포워딩되어도 인증 로직이 중복으로 실행되지 않게 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1767772147506&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
protected void doFilterInternal (
    @NonNull HttpServletRequest request,
    @NonNull HttpServletResponse response,
    @NonNull FilterChain filterChain
) throws ServletException, IOException {

    // 1. 요청 헤더에서 토큰 추출
    String accessToken = resolveToken(request);

    // 2. 토큰이 유효한지 검증
    if (accessToken != null &amp;amp;&amp;amp; jwtProvider.validateToken(accessToken)) {
        // 토큰이 유효하다면 인증 객체를 가져와서 SecurityContext에 담음
        Authentication auth = jwtProvider.getAuthentication(accessToken);
        SecurityContextHolder.getContext().setAuthentication(auth);
    }

    // 3. 다음 필터로 넘김
    filterChain.doFilter(request, response);
}&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;resolveToken : 클라이언트가 보낸 Http 헤더(Authorization: Bearer &amp;lt;토큰&amp;gt;)에서의 앞에 Bearer 글자를 떼어내고 순수 토큰값만 추출&lt;/li&gt;
&lt;li&gt;validationToken(accessToken) : JwtProvider에게 이 토큰에 대한 검증을 요청함&lt;/li&gt;
&lt;li&gt;getAuthentication(accessToken) : 토큰이 진짜라면 JwtProvider가 DB를 조회해 사용자 정보를 가져오고, 스프링 시큐리티용 신분증(=Authentication)을 만듦&lt;/li&gt;
&lt;li&gt;SecurityContextHolder.getContext().setAuthetication(auth) : 서버의 임시 메모리(SecurityContext)에 이 신분증을 넣어둠. 이 과정을 통해 이 요청이 끝날 때까지 서버는 &quot;인증된 사용자&quot;라고 인식함&lt;/li&gt;
&lt;li&gt;filterChain.doFilter() : 필터의 검증 후 다음 필터로 넘어가도록 요청을 넘김&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #6164c6;&quot;&gt;&lt;b&gt;참고! protected란?&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 패키지에 있는 모든 클래스, 혹은 다른 패키지에 있더라도 해당 클래스를 상속받은 자식 클래스에서 사용할 수 있도록 접근을 허용하는 접근 제한자. 자세한건 &lt;a href=&quot;https://www.notion.so/24-05-22-8-Object-Class-1112906797b44cc8aa49674681d3942f?source=copy_link#9a9f92d98ce74363913109539ffe6be3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기 참조.&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767772783850&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;24.05.22 | 8일차 정리  - Object , Class | Notion&quot; data-og-description=&quot;부자되게 해주세용&quot; data-og-host=&quot;confirmed-clave-f2a.notion.site&quot; data-og-source-url=&quot;https://www.notion.so/24-05-22-8-Object-Class-1112906797b44cc8aa49674681d3942f?source=copy_link#9a9f92d98ce74363913109539ffe6be3&quot; data-og-url=&quot;https://confirmed-clave-f2a.notion.site/24-05-22-8-Object-Class-1112906797b44cc8aa49674681d3942f&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Eiovk/hyZQ0TOkvK/uYKgKlm47KzrRkSW4DaVY1/img.png?width=808&amp;amp;height=158&amp;amp;face=0_0_808_158,https://scrap.kakaocdn.net/dn/fsjAI/hyZRbueTqX/kUDOzVBa3UehfTH9KdGPmK/img.png?width=808&amp;amp;height=158&amp;amp;face=0_0_808_158&quot;&gt;&lt;a href=&quot;https://www.notion.so/24-05-22-8-Object-Class-1112906797b44cc8aa49674681d3942f?source=copy_link#9a9f92d98ce74363913109539ffe6be3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.notion.so/24-05-22-8-Object-Class-1112906797b44cc8aa49674681d3942f?source=copy_link#9a9f92d98ce74363913109539ffe6be3&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Eiovk/hyZQ0TOkvK/uYKgKlm47KzrRkSW4DaVY1/img.png?width=808&amp;amp;height=158&amp;amp;face=0_0_808_158,https://scrap.kakaocdn.net/dn/fsjAI/hyZRbueTqX/kUDOzVBa3UehfTH9KdGPmK/img.png?width=808&amp;amp;height=158&amp;amp;face=0_0_808_158');&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;24.05.22 | 8일차 정리  - Object , Class | Notion&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;confirmed-clave-f2a.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #6164c6;&quot;&gt;&lt;b&gt;참고! 패키지란?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;130&quot; data-origin-height=&quot;48&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGaku3/dJMcachuAcB/17B1kjc2OwcYopsGKN6F91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGaku3/dJMcachuAcB/17B1kjc2OwcYopsGKN6F91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGaku3/dJMcachuAcB/17B1kjc2OwcYopsGKN6F91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGaku3%2FdJMcachuAcB%2F17B1kjc2OwcYopsGKN6F91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;130&quot; height=&quot;48&quot; data-origin-width=&quot;130&quot; data-origin-height=&quot;48&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;예를 들어 com.dalmuri와 example.dalmuri는 서로 다른 패키지이다.&lt;/p&gt;
&lt;pre id=&quot;code_1767773043121&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  java
 ├─   com.dalmuri
 │   └─ ...
 └─   example.dalmuri&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 com.dalmuri와 com.example은&lt;/p&gt;
&lt;pre id=&quot;code_1767773107898&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  com
 ├─   dalmuri
 │   └─ ...
 └─   example
     └─ ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;com까진 같은 패키지로 공유하지만 그 뒤부턴 다른 패키지로 분류된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #6164c6;&quot;&gt;&lt;b&gt;참고! 왜 no usage가 뜰까&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;287&quot; data-origin-height=&quot;47&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/loO3l/dJMcaaYh4Ej/cYu74rSbfgdBgnnFDffBY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/loO3l/dJMcaaYh4Ej/cYu74rSbfgdBgnnFDffBY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/loO3l/dJMcaaYh4Ej/cYu74rSbfgdBgnnFDffBY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FloO3l%2FdJMcaaYh4Ej%2FcYu74rSbfgdBgnnFDffBY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;287&quot; height=&quot;47&quot; data-origin-width=&quot;287&quot; data-origin-height=&quot;47&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저렇게 no usage가 뜨는 이유? 호출을 안하는데 사용이 되는거 같다...&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;호출의 주체가 '나'가 아닌 '프레임워크'이기 때문 : SecurityConfig에서 .addFilterBefore()라고 등록하면 Spring Security 프레잌워크가 내부적으로 필터 체인을 돌리면서 이 메소드를 대신 호출한다&lt;/li&gt;
&lt;li&gt;리플렉션 기반 동작? : IntelliJ의 Usage추적 기능은 주로 소스 코드상에서 직접적으로 명시된 호출부를 찾는데, 프레임워크 내부 깊숙한 곳에서 일어나는 호출은 검색 결과에 잡히지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1767773362430&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 헤더에서 &quot;Bearer&quot;를 제외한 실제 토큰만 꺼내는 메소드
private String resolveToken(HttpServletRequest request) {
    String bearerToken = request.getHeader(&quot;Authorization&quot;);
    if (StringUtils.hasText(bearerToken) &amp;amp;&amp;amp; bearerToken.startsWith(&quot;Bearer&quot;)) {
        return bearerToken.substring(7);
    }
    return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 토큰을 보낼 때 'Bearer dfkldfkdjkldlf...'이런식으로 보내도록 작성을 하였으므로 substring(7)을 통해 진짜 토큰값을 추출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;... 이 과정을 다 걸쳐서 통과해야 드디어 Controller에 갈 수 있다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[참고 자료]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ittrue.tistory.com/129&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ittrue.tistory.com/129&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767773458967&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Java] 자바 패키지(Package)와 임포트(Import) 개념 정리 및 활용&quot; data-og-description=&quot;패키지 (Package) 자바에서 패키지란 특정한 목적을 공유하는 클래스와 인터페이스의 묶음을 의미한다. 즉, 서로 관련 있는 클래스들을 묶어 효과적으로 관리하기 위해 사용한다. 자바의 패키지는&quot; data-og-host=&quot;ittrue.tistory.com&quot; data-og-source-url=&quot;https://ittrue.tistory.com/129&quot; data-og-url=&quot;https://ittrue.tistory.com/129&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eVijw/hyZPITvExl/arc4OLlqGH3gFIF2YVXHbk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bfhM3n/hyZQJJ3sdx/yiZaa4yhgrI2vvkC7hE07K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/owbK2/hyZQ335tHA/Oxn1XpG9zlJ2YQRsnarOsk/img.jpg?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360&quot;&gt;&lt;a href=&quot;https://ittrue.tistory.com/129&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ittrue.tistory.com/129&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eVijw/hyZPITvExl/arc4OLlqGH3gFIF2YVXHbk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bfhM3n/hyZQJJ3sdx/yiZaa4yhgrI2vvkC7hE07K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/owbK2/hyZQ335tHA/Oxn1XpG9zlJ2YQRsnarOsk/img.jpg?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Java] 자바 패키지(Package)와 임포트(Import) 개념 정리 및 활용&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;패키지 (Package) 자바에서 패키지란 특정한 목적을 공유하는 클래스와 인터페이스의 묶음을 의미한다. 즉, 서로 관련 있는 클래스들을 묶어 효과적으로 관리하기 위해 사용한다. 자바의 패키지는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ittrue.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Chm7h/dJMcadObcip/Z2YHJ09q1YyXgVS3LlUm40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Chm7h/dJMcadObcip/Z2YHJ09q1YyXgVS3LlUm40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Chm7h/dJMcadObcip/Z2YHJ09q1YyXgVS3LlUm40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FChm7h%2FdJMcadObcip%2FZ2YHJ09q1YyXgVS3LlUm40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;300&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Backend ️/Java☕(Spring )</category>
      <category>authentication</category>
      <category>authorization</category>
      <category>FilterChain</category>
      <category>java</category>
      <category>java 21</category>
      <category>jwt</category>
      <category>JwtFilter</category>
      <category>jwtProvider</category>
      <category>Spring Security</category>
      <author>JanuDev</author>
      <guid isPermaLink="true">https://janudev.tistory.com/56</guid>
      <comments>https://janudev.tistory.com/56#entry56comment</comments>
      <pubDate>Wed, 7 Jan 2026 17:16:44 +0900</pubDate>
    </item>
    <item>
      <title>Axios를 써보자</title>
      <link>https://janudev.tistory.com/55</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PVN4S/dJMcaiBXtDo/62fzpPnmG55foCqGxnlViK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PVN4S/dJMcaiBXtDo/62fzpPnmG55foCqGxnlViK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PVN4S/dJMcaiBXtDo/62fzpPnmG55foCqGxnlViK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPVN4S%2FdJMcaiBXtDo%2F62fzpPnmG55foCqGxnlViK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;544&quot; height=&quot;161&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 로그인 구현중 클라이언트로부터 서버로 request를 보낼 때 앞으로 무조건 header에 발급된 토큰값을 집어넣으라는데...&lt;/p&gt;
&lt;pre id=&quot;code_1767675583980&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const response = await fetch('http://localhost:3001/login', {
   method : 'POST',
   headers : {
     'Content-Type' : 'application/json',
     'Authorization' : 토큰어쩌구저쩌구
   },
   body : JSON.stringify(user)
 })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 이렇게 작성할 수 없는 노릇이라 찾아보니 Axios를 사용하라고 하신다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axios는 회사에서도 사용해봤는데 &lt;span style=&quot;color: #dddddd;&quot;&gt;솔직히 무슨 뜻인진 잘 모르고 다른분들 다 쓰길래 나도 따라쓴 격이라&lt;/span&gt; 나도 이번에 제대로 공부해볼겸.. Axios에 대해 공부해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axios 공식 웹사이트는 &lt;a href=&quot;https://axios-http.com/kr/docs/intro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Axios란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저와 Node.js에서 사용할수 있는 Promise 기반의 Http 클라이언트 라이브러리로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트 내에서도 fetch API가 있지만&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;자동 파싱 : 서버로부터 받은 JSON 데이터를 자동으로 JavaScript객체로 변환시킴(JSON.parse 안해도 됨)&lt;/li&gt;
&lt;li&gt;요청, 응답을 중간에 가로챔(Interceptor)&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;사실 나도 Jwt 토큰을 집어넣기 위해서라도 request때 인터셉트가 필요하다...&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. 구조&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. request 구조&lt;/h4&gt;
&lt;pre id=&quot;code_1767676610519&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  method: 'get',	// 필수
  url: '/example',     // 필수
  baseURL: '',
  haeaders: {},
  params: {},
  data: {},
  timeout: 0
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 메소드로 나눠쓸 뿐 하나의 객체로 정리된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 대표적인 4개의 메소드로 작성해보자.&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;1️⃣Get&lt;/p&gt;
&lt;pre id=&quot;code_1767677275693&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;axios.get(url, config)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 쓸 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1767677197297&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;axios.get('/users', {
  params: { page: 1 },
  headers: {
    Authorization: 'Bearer 토큰'
  }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성하면, request객체는&lt;/p&gt;
&lt;pre id=&quot;code_1767677211867&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  method: 'get',
  url: '/users',
  params: { page: 1 },
  headers: { Authorization: 'Bearer 토큰' }
}&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;2️⃣Post&lt;/p&gt;
&lt;pre id=&quot;code_1767677302706&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;axios.post(url, data 객체, config)&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_1767677320488&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;axios.post(
  '/login',
  { id: 'admin', pw: '1234' },
  { timeout: 3000 }
)&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_1767677609182&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  method: 'post',
  url: '/login',
  data: { id: 'admin', pw: '1234' },
  timeout: 3000
}&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;3️⃣Delete&lt;/p&gt;
&lt;pre id=&quot;code_1767679752065&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;axios.delete(url, config)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Delete는 원래 data 자리가 없기 때문에 작성하고 싶으면 다음과 같이 써야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1767679821643&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;axios.delete('/example', {
  data : { reason: '탈퇴' }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4️⃣Put&lt;/p&gt;
&lt;pre id=&quot;code_1767680280771&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;axios.put(url, data, config)&lt;/code&gt;&lt;/pre&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;2. .then(), catch.(), finally.()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request를 보낸 뒤 성공/실패/항상 실행 여부에 따라 뒤에 .then(), .catch(), .finally()를 덧붙힐 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1767680565685&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;axios.post('/example', {
  name: 'Fred',
  last: 'Flintstone',
})
.then (function (response){
  // 성공 핸들링
  console.log(response)
})
.catch (function (error){
  // 에러 핸들링
  console.log(error)
})
.finally(function (){
  // 항상 실행되는 영역
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1️⃣.then() &amp;mdash; 성공했을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청&amp;amp;응답이 성공했을 때 실행할 함수를 작성하는 영역. 성공한 Promise만 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2️⃣.catch() &amp;mdash; 실패했을 때 &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청&amp;amp;응답이 실패했을 때 실행할 함수를 작성하는 영역. 실패한 Promise만 받는다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767680694705&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;error = {
  message: '',
  response: {
    status: 404,
    data: {},
    headers: {}
  }
}&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_1767680718563&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.catch(error =&amp;gt; {
  console.log(error.response.status)
  console.log(error.response.data)
})&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;3️⃣finally &amp;mdash; 성공이든 실패든 무조건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;finally의 용도는&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;버튼 다시 활성화&lt;/li&gt;
&lt;li&gt;스피너 끄기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같이 결과와 상관 없이 실행할 때 정리하는 담당을 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. response 구조&lt;/h4&gt;
&lt;pre id=&quot;code_1767680925087&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;response = {
  data: 서버가 준 실제 데이터 
  status: 200,
  headers: {},
  config: 내가 보낸 설정,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response 객체는 서버에서 가져온 데이터를 화면에 뿌리거나 상황을 확인하는 용도의 정보들이 담겨 있다.&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;4. Axios 인스턴스 - 공통&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axios를 사용하기 위해선 공통 인스턴스에 대한 기본 설정이 필요하다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767681110086&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const apiInstance = axios.create({
  baseURL: '요청 보낼 백단의 URL',
  timeout: 30000, // 밀리세컨초 기준
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면 단순히 api.get('/example')로 사용할 수도 있지만 매번 반복하기엔 관리가 너무 불편하고 비효율적이다.&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;모든 요청마다 전체 url을 작성해야 함&lt;/li&gt;
&lt;li&gt;서버 주소가 바뀌면 모든 코드를 수정해야 함&lt;/li&gt;
&lt;li&gt;공통적인 헤더(인증 토큰 등)를 매번 설정해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스를 만들어야 공통설정을 한 곳에 모아두고 재사용할 수 있다.&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;1️⃣axios.create()&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;나만의 커스텀된 Axios 객체. 새로운 axios 인스턴스 생성할 때 사용한다.&lt;/li&gt;
&lt;li&gt;인스턴스 설정 파일 한곳에 작성하면 여러 컴포넌트에서 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2️⃣baseURL&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 API 요청 앞에 붙는 주소&lt;/li&gt;
&lt;li&gt;실제 호출될 땐 apiInstance.get('/example')과 같이 상대 경로만 적으면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3️⃣timeout&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;서버에 문제가 생겨 응답이 오지 않을 때 무한적 기다리지 않고 연결을 끊어 브라우저의 리소스를 보호한다(30000 = 30초)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Axios 인스턴스 - 인터셉터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 보내기 직전 혹은 응답을 받은 직후에 특정 로직을 실행할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1767681915733&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiInstance.interceptors.request.use((config) =&amp;gt; {
  const token = localStorage.getItem('accessToken')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
}, (error) =&amp;gt; Promise.reject(error))&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;apiInstance : 위에서 만든 커스텀 axios(axios.create()로 생성된 커스텀 axios)&lt;/li&gt;
&lt;li&gt;interceptors : 요청/응답 흐름 중간에 끼어듦&lt;/li&gt;
&lt;li&gt;request : 요청단계, 즉 서버로 날아가기 직전에 실행한다는 의미 (&amp;harr; interceptors.response)&lt;/li&gt;
&lt;li&gt;use : '이 가로채기 규칙을 등록한다'는 뜻. 함수를 Axios에 맡기는 행위&lt;/li&gt;
&lt;li&gt;localStorage.getItem('accessToken') : 기존 로직에서 로컬스토리지에 저장했던 accessToken을 가져옴. 이 토큰은 jwt에서 발급&lt;/li&gt;
&lt;li&gt;config.header.Authorization : 요청 헤더의 Authorization 항목에 'Bearer 토큰값'을 작성&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;  axios 구현 이유&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;return config : 설정이 완료된 config를 반환 - 실제 요청이 서버로 출발&lt;/li&gt;
&lt;li&gt;Promise.reject(error) : 에러 발생 시 Promoise.reject로 반환&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://axios-http.com/kr/docs/example&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://axios-http.com/kr/docs/example&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767684456273&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;기본 예제 |&amp;nbsp;Axios Docs&quot; data-og-description=&quot;기본 예제 Axios를 사용하기 위한 기본 예제 참고: CommonJS 사용법 require()를 이용한 CommonJS를 사용하는 동안 TypeScript 타이핑(인텔리센스 / 자동 완성)을 사용하려면, 다음 방법을 쓰세요. const axios = r&quot; data-og-host=&quot;axios-http.com&quot; data-og-source-url=&quot;https://axios-http.com/kr/docs/example&quot; data-og-url=&quot;https://axios-http.com/kr/docs/example&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://axios-http.com/kr/docs/example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://axios-http.com/kr/docs/example&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;기본 예제 |&amp;nbsp;Axios Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;기본 예제 Axios를 사용하기 위한 기본 예제 참고: CommonJS 사용법 require()를 이용한 CommonJS를 사용하는 동안 TypeScript 타이핑(인텔리센스 / 자동 완성)을 사용하려면, 다음 방법을 쓰세요. const axios = r&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;axios-http.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@shin6403/React-axios%EB%9E%80-feat.-Fetch-API&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@shin6403/React-axios%EB%9E%80-feat.-Fetch-API&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767684431717&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;[React] axios란? (feat. Fetch API)&quot; data-og-description=&quot;# Intro 리액트는 효율적인 UI 구현을 위한 라이브러리이다. HTTP Client(HTTP 상에서 커뮤니케이션을 하는 자바 기반 컴포넌트)를 내장하고 있는 Angular와는 다르게, 리액트는 따로 내장 클래스가 존재&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@shin6403/React-axios%EB%9E%80-feat.-Fetch-API&quot; data-og-url=&quot;https://velog.io/@shin6403/React-axios란-feat.-Fetch-API&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b4zfJR/hyZQZApFUn/MmchhHyBSd5qQLmkXIYpM0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/ddyDdw/hyZPGH7hIZ/VbW92nIoCZ4t3lQpNMVLHk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/bav8Js/hyZRbt3cak/Evolm5pV1ETnYW4H5xqd21/img.jpg?width=3024&amp;amp;height=3024&amp;amp;face=675_869_1823_1232&quot;&gt;&lt;a href=&quot;https://velog.io/@shin6403/React-axios%EB%9E%80-feat.-Fetch-API&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@shin6403/React-axios%EB%9E%80-feat.-Fetch-API&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b4zfJR/hyZQZApFUn/MmchhHyBSd5qQLmkXIYpM0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/ddyDdw/hyZPGH7hIZ/VbW92nIoCZ4t3lQpNMVLHk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/bav8Js/hyZRbt3cak/Evolm5pV1ETnYW4H5xqd21/img.jpg?width=3024&amp;amp;height=3024&amp;amp;face=675_869_1823_1232');&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;[React] axios란? (feat. Fetch API)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;# Intro 리액트는 효율적인 UI 구현을 위한 라이브러리이다. HTTP Client(HTTP 상에서 커뮤니케이션을 하는 자바 기반 컴포넌트)를 내장하고 있는 Angular와는 다르게, 리액트는 따로 내장 클래스가 존재&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chihoya.tistory.com/28&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://chihoya.tistory.com/28&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767684438358&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;Ajax, Axios란??&quot; data-og-description=&quot;Ajax Asynchronous Javascript and XML(비동기식 자바스크립트와 xml)의 약자이다. 서버와 비동기적으로 데이터 주고받는 JS기술을 의미한다. Ajax를 이해하기 위해선 Server를 알아야한다. 왜? Ajax는 서버랑 통&quot; data-og-host=&quot;chihoya.tistory.com&quot; data-og-source-url=&quot;https://chihoya.tistory.com/28&quot; data-og-url=&quot;https://chihoya.tistory.com/28&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/i7xKX/hyZQYnYQfI/GAc9AuD5Vh59ASyMX85ltk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cW8v3j/hyZQ67lND3/3Gc3yvp7TaaZQfTyMAycS1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://chihoya.tistory.com/28&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chihoya.tistory.com/28&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/i7xKX/hyZQYnYQfI/GAc9AuD5Vh59ASyMX85ltk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cW8v3j/hyZQ67lND3/3Gc3yvp7TaaZQfTyMAycS1/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;Ajax, Axios란??&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Ajax Asynchronous Javascript and XML(비동기식 자바스크립트와 xml)의 약자이다. 서버와 비동기적으로 데이터 주고받는 JS기술을 의미한다. Ajax를 이해하기 위해선 Server를 알아야한다. 왜? Ajax는 서버랑 통&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chihoya.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EZzEn/dJMcafyvJqF/UJroT58Wmv25KT27anmTlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EZzEn/dJMcafyvJqF/UJroT58Wmv25KT27anmTlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EZzEn/dJMcafyvJqF/UJroT58Wmv25KT27anmTlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEZzEn%2FdJMcafyvJqF%2FUJroT58Wmv25KT27anmTlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;300&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend /JavaScript , TypeScript </category>
      <category>axios</category>
      <category>axios api</category>
      <category>http</category>
      <category>javascript</category>
      <category>promise</category>
      <category>REST API</category>
      <category>비동기</category>
      <category>서버</category>
      <category>인터셉터</category>
      <category>통신</category>
      <author>JanuDev</author>
      <guid isPermaLink="true">https://janudev.tistory.com/55</guid>
      <comments>https://janudev.tistory.com/55#entry55comment</comments>
      <pubDate>Tue, 6 Jan 2026 16:28:55 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] JWT 이용해서 로그인 기능 만들기</title>
      <link>https://janudev.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JWT = 양파&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXg5u7/dJMcagjKQFp/Ap9SJar5qmhkqvkzxCWha1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXg5u7/dJMcagjKQFp/Ap9SJar5qmhkqvkzxCWha1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXg5u7/dJMcagjKQFp/Ap9SJar5qmhkqvkzxCWha1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXg5u7%2FdJMcagjKQFp%2FAp9SJar5qmhkqvkzxCWha1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;800&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자꾸 공부하면 공부할수록 뭔 양파마냥 알아야 할게 자꾸 나온다.. 미쳐버릴지경&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;사용자 검증 UserDetailsService&lt;/li&gt;
&lt;li&gt;비밀번호 비교 PasswordEncoder&lt;/li&gt;
&lt;li&gt;Authentication 객체 생성&lt;/li&gt;
&lt;li&gt;JWT 발급&lt;/li&gt;
&lt;li&gt;클라이언트에 토큰 전달&lt;/li&gt;
&lt;li&gt;Filter 구현&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서대로 작성할 예정이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;작성 순서&lt;/h2&gt;
&lt;pre id=&quot;code_1765945186889&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {

    private final AuthenticateHandler authenticateHandler;
    private final JwtProvider jwtProvider;
    private final UserDetailsServiceImpl userDetailsServiceImpl;

    public TokenDTO login(LoginRequestDTO request) {
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        /*
        * (1) 사용자 검증(UserDetailsService)
        * */
        UserDetailsDTO userDetails = (UserDetailsDTO) userDetailsServiceImpl.loadUserByUsername(request.getUserId());

        /*
        * (2) 비밀번호 비교(PasswordEncoder)
        * */
        if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
            throw new BadCredentialsException(&quot;비밀번호가 일치하지 않아!(T_T)&quot;);
        }

        /*
        * (3) Authentication 객체 생성
        * */
        Authentication authentication = authenticateHandler.authenticate(request);

        // (4) JWT 발급
        String accessToken = jwtProvider.createAccessToken(authentication);

        // (5) 클라이언트에 토큰 전달
        return new TokenDTO(accessToken);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 사용자 검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 유저 정보를 조회하고 UserDetails 객체를 만든다. ex) 이 username을 가진 유저가 있나요? &amp;rarr; UserDetailsImpl 만들어서 스프링 시큐리티에 제공한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인은 2단계로 이루어지는데, 첫번째는 사용자 검증(Authentication 준비), 두번째는 비밀번호 검증(Authentication 완료) 단계이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 검증은 &quot;너가 누군지&quot;, 즉 이 userId를 가진 사람이 실제로 존재하는지, 그리고 DB에 등록된 사용자인지 검증을 한다.&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;이 2단계가 섞이면 위험하기 때문에 시큐리티는 일부러 나눠놨다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;역할&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;책임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;UserDetailsService&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;이 사용자가 존재하는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;UserDetails&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;이 사용자의 정보는 무엇인지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;AuthenticationProvider&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;비밀번호가 맞는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;SecurityContext&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;로그인 완료 후 상태 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리하여 사용자 검증 단계에선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1251&quot; data-start=&quot;1234&quot;&gt;❌ 로그인 성공 처리 안 함&lt;/li&gt;
&lt;li data-end=&quot;1267&quot; data-start=&quot;1252&quot;&gt;❌ 세션/JWT 안 만듦&lt;/li&gt;
&lt;li data-end=&quot;1292&quot; data-start=&quot;1268&quot;&gt;⭕ &amp;ldquo;이 사람이 누구인지 설명서만 전달&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 처리하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://janudev.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;코드에 대한 설명은 너무 길어져서 따로 분리했다.&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 비밀번호 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력한 비밀번호를 암호화했을 때 DB의 암호값고 동일한지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 과정은 Authentication 과정이 있으면 굳이 필요하진 않다고 하는데.. 그냥 공부차 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1767142227171&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
  throw new BadCredentialsException(&quot;비밀번호가 일치하지 않아!&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) PasswordEncoder&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호를 안전하게 암호화하고, 입력된 비밀번호와 저장된 암호회된 비밀번호를 비교하는 인터페이스&lt;/p&gt;
&lt;pre id=&quot;code_1767146160344&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface PasswordEncoder {
	
　　// 비밀번호를 단방향 암호화
　　String encode(CharSequence rawPassword);
	
　　// 암호화되지 않은 비밀번호(raw-)와 암호화된 비밀번호(encoded-)가 일치하는지 비교
　　boolean matches(CharSequence rawPassword, String encodedPassword);
	
　　// 암호화된 비밀번호를 다시 암호화하고자 할 경우 true를 return하게 설정
　　default boolean upgradeEncoding(String encodedPassword) { return false; };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 구성되어 있고, PasswordEncoder 구현 클래스는 4개가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BcryptPasswordEncoder : BCrypt 해시 함수를 사용하여 비밀번호를 암호화&lt;/li&gt;
&lt;li&gt;Argon2PasswordEncoder: Argon2 해시 함수를 사용해 비밀번호를 암호화&lt;/li&gt;
&lt;li&gt;Pbkdf2PasswordEncoder : PBKDF2 해시 함수를 사용하여 비밀번호를 암호화&lt;/li&gt;
&lt;li&gt;SCryptPasswordEncoder : SCrypt 해시 함수를 사용해서 비밀번호 암호화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 BCryptPasswordEncoder를 이용했다. 단순한 해싱이 아닌 솔트(Salt)라는 렌덤 데이터를 추가하여 보안성을 높인다. 동일한 비밀번호도 암호화에 따라 결과값이 달라지기 때문에 원래 비밀번호를 알아내기 매우 어렵다.&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;(2) matches(평문, 암호문)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 암호문(userDetails.getPassword())에서 사용됬던 솔트(Salt)값을 추출한다. 추출된 솔트와 입력받은 평문 비밀번호를 결합하여 다시 해싱한 후, 그 결과가 저장된 암호문과 같은지 확인한다.&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;(3) BadCredentialsException&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서 제공하는 표준 예외로, 자격증명(id/pw)이 유효하지 않을 때 발생한다. 사용자의 id에 해당하는 패스워드가 일치하지 않을 때, 사용자의 아이디가 전달되지 않을 때 등등!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예외가 발생하면 보통 Spring Security의 인증 필터가 이를 가로채서 로그인 실패 페이지로 리다이렉트하거나, 401 unauthorized 에러 응답을 보내게 된다&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;3. Authentication 객체 생성&lt;/h3&gt;
&lt;pre id=&quot;code_1767153857263&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final AuthenticateHandler authenticateHandler;
Authentication authentication = authenticateHandler.authenticate(request);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authentication 객체를 생성하는 과정은 &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;&lt;b&gt;&lt;span style=&quot;color: #a6bc00;&quot;&gt;Authentication 객체란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서 Authentication은 현재 로그인한 사용자의 신원 보증서이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Principal: 사용자의 아이디 혹은 사용자 객체&lt;/li&gt;
&lt;li&gt;Credentials: 비밀번호(인증에 사용된 증거)&lt;/li&gt;
&lt;li&gt;Authorities: 사용자가 가진 권한&lt;/li&gt;
&lt;li&gt;Authenticated: 인증 여부(T/F)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) AuthenticationManager 수동 생성&lt;/h4&gt;
&lt;pre id=&quot;code_1767157376898&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public AuthenticationManager authenticationManager() throws Exception {
  return authenticationConfiguration.getAuthenticationManager();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AuthenticationManager은 &quot;인증 센터의 총책임자&quot;이다. 말 그대로 인증(Authentication)을 처리하는 관리자인데, 사용자가 로그인을 시도하면 그 정보가 맞는지 확인한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예전 Spring Security는 WebSecurityConfigurerAdapter이라는걸 상속받으면 자동으로 AuthenticationManager을 생성해줬는데, 요즘은(5.7버전 이상)은 컴포넌트 기반 설정으로 바뀌면서 보안상의 이유로 자동 Bean 생성은 안해주고 개발자가 명시해야 한다고 한다.............&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;그래서 Bean으로 등록을 해줬다.&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;AuthenticationConfiguration : 스프링이 내부적으로 만들어둔 인증 설정 정보&lt;/li&gt;
&lt;li&gt;.getAuthenticationManager() : 이미 설정된 정보(UserDetailsService, PasswordEncoder 등)를 바탕으로 완성한 &quot;총책임자(Manager)&quot;를 꺼내오는 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) AuthenticationHandler&amp;nbsp;&lt;/h4&gt;
&lt;pre id=&quot;code_1767157257483&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service  
@Slf4j
@RequiredArgsConstructor
public class AuthenticateHandler {

    private final AuthenticationManager authenticationManager;

    public Authentication authenticate (LoginRequestDTO request) {
        try {
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            request.getUserId(),
                            request.getPassword()
                    )
            );
        } catch (BadCredentialsException e) {
            log.warn(&quot;인증 실패 - 사용자: {} (잘못된 비밀번호)&quot;, request.getUserId());
            throw e;
        } catch (UsernameNotFoundException e) {
            log.warn(&quot;인증 실패 - 사용자: {} (존재하지 않는 계정)&quot;, request.getUserId());
            throw e;
        } catch (Exception e) {
            log.error(&quot;인증 중 오류 발생 - 사용자: {}&quot;, request.getUserId(), e);
            throw e;
        }

    } // authenticate

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ UsernamePasswordAuthenticationToken() 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 보낸 아이디와 비밀번호를 담은 &lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인증 요청용 토큰&lt;/span&gt;&lt;/b&gt;을 먼저 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 아직 검증되지 않았기 때문에&lt;span style=&quot;color: #0593d3;&quot;&gt; setAuthenticated(false)&lt;/span&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;2️⃣ AuthenticationManager.authenticate() 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 &quot;신청서&quot;를 AuthenticationManager에게 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자는 내부적으로 등록된 AuthenticationProvider들을 찾아 &lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;이 아이디와 비밀번호가 진짜 맞는지&lt;/b&gt;&lt;/span&gt; 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ 최종 Authentication 객체 반환(인증 완료)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 검증(비밀번호 비교 등)이 성공하면, 사용자의 권한(Roles)과 상세 정보가 포함된 새로운 Authentication 객체를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 객체는&lt;span style=&quot;color: #0593d3;&quot;&gt; setAuthenticatted(true)&lt;/span&gt;상태이며, 이제 시스템 어디서든 &quot;이 사람은 믿을 수 있는 사용자다&quot; 라고 인정받게 된다.&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;4. JWT 발급&lt;/h3&gt;
&lt;pre id=&quot;code_1767163021664&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class JwtProvider {

    private Long ACCESS_TOKEN;

    private Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    public String createAccessToken (Authentication authentication) {

        UserDetailsDTO principal = (UserDetailsDTO) authentication.getPrincipal();
        String userId = principal.getUserId();
        List&amp;lt;String&amp;gt; roles = principal.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();

        Date now = new Date();
        Date expiry = new Date(now.getTime() + ACCESS_TOKEN);

        return Jwts.builder()
                .setSubject(userId)
                .claim(&quot;role&quot;, roles)
                .setIssuedAt(now)
                .setExpiration(expiry)
                .signWith(secretKey, SignatureAlgorithm.HS256)
                .compact();

    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createAccessToken : 새로운 토큰을 생성하는 메소드&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ACCESS_TOKEN : application.yml 혹은 properties에서 설정된 만료시간 값을 가져온다&lt;/li&gt;
&lt;li&gt;secretKey : 서명(Signature)을 위한 키. HS256알고리즘에 적합한 안전 키를 자동으로 생성.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Keys.secretKeyFor는 서버가 재시작될 때 마다 새로운 키를 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 사용자 정보 추출(Payload 준비)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;authentication.getPrincipal() : 앞선 단계에서 생성했던 인증 객체에서 사용자 상세정보를 꺼냄(UserDetailsDTO). 토큰 내부에 저장할 사용자 ID와 권한목록을 준비&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) JWT 토큰 생성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setSubject : 토큰의 식별자. 보통 사용자 고유 아이디를 넣음&lt;/li&gt;
&lt;li&gt;claim(&quot;role&quot;, roles) : 표준 필드 외에 개발자가 추가하고 싶은 정보를 넣음&lt;/li&gt;
&lt;li&gt;setIssuedAt(now) : 토큰 발행 시간&lt;/li&gt;
&lt;li&gt;setExpiration(expiry) : 토큰 만료 시간&lt;/li&gt;
&lt;li&gt;signWith(secretKey, SignatureAlgorithm.HS256) : 암호화 서명. 준비한 secretKey와 HS256알고리즘을 사용해 토큰이 위조되지 않음을 증명하는 서명. 가장 중요한 보안 단계&lt;/li&gt;
&lt;li&gt;compact() : 모든 설정을 마치고 최종적으로 header.payload.signature 형태의 긴 문자열을 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 클라이언트에 토큰 전달&lt;/h3&gt;
&lt;pre id=&quot;code_1767166130462&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return new TokenDTO(accessToken);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Filter 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://janudev.tistory.com/56&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이것도 설명 길어질 것 같아서 따로 게시글 만들었다.&lt;/a&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;1. java.lang.StackOverflowError&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2325&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yfOui/dJMcabbGo6g/dB8qpgW0Ao81T3FX9rKTKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yfOui/dJMcabbGo6g/dB8qpgW0Ao81T3FX9rKTKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yfOui/dJMcabbGo6g/dB8qpgW0Ao81T3FX9rKTKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyfOui%2FdJMcabbGo6g%2FdB8qpgW0Ao81T3FX9rKTKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2325&quot; height=&quot;310&quot; data-origin-width=&quot;2325&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드 호출이 너무 깊어져서 스택 메모리가 가득 찼을 때 발생하는 에러이다.&lt;/p&gt;
&lt;pre id=&quot;code_1765858274172&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:216) ~[spring-aop-6.1.10.jar:6.1.10]
	at jdk.proxy3/jdk.proxy3.$Proxy69.authenticate(Unknown Source) ~[na:na]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 무한반복되는걸 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthenticateHandler와 같이 Authentication과 관련된 과정에서 무언가가 무한반복해서 호출하는거라는건 어림짐작 할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AOP 프록시와 관련된 재귀문제라고 한다.&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;h4 data-ke-size=&quot;size20&quot;&gt;GPT의 추가설명  &amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size18&quot;&gt;1. 프록시 객체 ($Proxy69)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;$Proxy69.authenticate는 @Service나 @Component와 같은 Spring Bean에 트랜잭션(@Transactional)이나 보안(AOP)과 같은 부가 기능이 적용될 때, **Spring이 원본 객체 대신 생성하여 주입하는 대리 객체(Proxy)**입니다.&lt;/li&gt;
&lt;li&gt;당신의 코드에서는 &lt;b&gt;AuthenticationManager&lt;/b&gt; 빈이 이 프록시 객체로 주입된 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size18&quot;&gt;2. 프록시의 invoke&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JdkDynamicAopProxy.invoke는 이 프록시 객체의 메서드(여기서는 authenticate)가 호출될 때 &lt;b&gt;항상 먼저 실행되는&lt;/b&gt; Spring AOP의 핵심 로직입니다. 이 로직은 부가 기능을 처리한 후 &lt;b&gt;원본 객체의 메서드를 호출&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size18&quot;&gt;3. 무한 재귀 (Stack Overflow)&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;StackOverflowError가 발생했다는 것은 다음과 같은 &lt;b&gt;순환 호출 구조&lt;/b&gt;가 발생했다는 의미입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;시작:&lt;/b&gt; AuthService (또는 외부) &lt;span data-math=&quot;\rightarrow&quot;&gt;$\rightarrow$&lt;/span&gt; &lt;b&gt;프록시 AuthenticationManager의 authenticate() 호출&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프록시:&lt;/b&gt; JdkDynamicAopProxy.invoke 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제:&lt;/b&gt; invoke 로직이 **원본 AuthenticationManager**를 호출하는 대신, 어떤 경로를 통해 &lt;b&gt;다시 1번 단계로 돌아가&lt;/b&gt; 프록시 객체의 authenticate()를 재호출합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;이 &lt;b&gt;무한 루프&lt;/b&gt;의 원인은 앞선 답변에서 설명드렸듯이, &lt;b&gt;AuthenticationManager 빈을 정의하는 과정&lt;/b&gt;(SecurityConfig)과 &lt;b&gt;이를 사용하는 커스텀 인증 로직&lt;/b&gt;(AuthenticateHandler 또는 AuthService) 간의 &lt;b&gt;의존성 주입(DI) 충돌&lt;/b&gt;이나 &lt;b&gt;설정의 중복&lt;/b&gt; 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론 :&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 발생 지점은 AuthenticationManager의 authenticate()의 호출 과정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 Spring Security의 설정과 커스텀 로직 간의 순환 참조(Circular Dependency)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB에서 직접 사용자정보 조회(getUserDetails)&lt;/li&gt;
&lt;li&gt;비밀번호를 직접 비교(passwordEncoder.matches)&lt;/li&gt;
&lt;li&gt;authenticatHandler.authenticate(request)를 통해 다시 인증 시도&lt;/li&gt;
&lt;li&gt;즉, 로그인 로직 안에서 Spring Security의 인증을 '수동' + '자동'으로 동시에 돌리면서 인증 과정이 자기 자신을 계속 호출하여 stackOverflow 발생 (길어져서 접은글 작성)&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;925&quot; data-start=&quot;868&quot; data-ke-size=&quot;size18&quot;&gt;authenticateHandler.authenticate() 안에서 무슨 일이 일어났나?&lt;/p&gt;
&lt;p data-end=&quot;969&quot; data-start=&quot;927&quot; data-ke-size=&quot;size16&quot;&gt;AuthenticateHandler는 내부에서 보통 이런 걸 하고 있어:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;span&gt;authenticationManager.authenticate(authenticationToken); &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1043&quot; data-start=&quot;1041&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;즉,  &lt;b&gt;Spring Security의 표준 인증 프로세스(AuthenticationManager)&lt;/b&gt; 를 다시 호출함&lt;/p&gt;
&lt;hr data-end=&quot;1116&quot; data-start=&quot;1113&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;1157&quot; data-start=&quot;1118&quot; data-ke-size=&quot;size18&quot;&gt;Spring Security는 이 인증을 어떻게 처리하나?&lt;/p&gt;
&lt;p data-end=&quot;1185&quot; data-start=&quot;1159&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security는 인증을 시작하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1373&quot; data-start=&quot;1187&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1214&quot; data-start=&quot;1187&quot;&gt;AuthenticationManager 실행&lt;/li&gt;
&lt;li data-end=&quot;1243&quot; data-start=&quot;1215&quot;&gt;AuthenticationProvider 호출&lt;/li&gt;
&lt;li data-end=&quot;1289&quot; data-start=&quot;1244&quot;&gt;UserDetailsService.loadUserByUsername() 호출&lt;/li&gt;
&lt;li data-end=&quot;1325&quot; data-start=&quot;1290&quot;&gt;(AOP / Filter / Security 설정에 따라)&lt;/li&gt;
&lt;li data-end=&quot;1373&quot; data-start=&quot;1326&quot;&gt;&lt;b&gt;커스텀 로그인 로직(AuthService.login())을 다시 타게 됨&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;1378&quot; data-start=&quot;1375&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;1410&quot; data-start=&quot;1380&quot; data-ke-size=&quot;size18&quot;&gt;그래서 무슨 일이 벌어졌나? (무한 루프)&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1767162757719&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;AuthService.login()
 └ authenticateHandler.authenticate()
     └ AuthenticationManager.authenticate()
         └ Spring Security 인증 시작
             └ AuthService.login() 다시 호출
                 └ authenticateHandler.authenticate()
                     └ AuthenticationManager.authenticate()
                         └ ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1782&quot; data-start=&quot;1742&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;끝없는 회전목마&lt;/b&gt;&lt;br /&gt;➡️ StackOverflowError 발생&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;문제를 해결하기 위해선 AuthenticationManager을 사용하지 않고, 이미 코드에선 직접 비밀번호 검증을 하고 있기때문에 인증 객체를 수동으로 생성하라고 하는데...&lt;/p&gt;
&lt;pre id=&quot;code_1765859296305&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AuthService {

    private final JdbcTemplate jdbcTemplate;
    private final AuthenticateHandler authenticateHandler;
    private final JwtProvider jwtProvider;

    public TokenDTO login(LoginRequestDTO request) {
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        UserDetailsImpl userDetails = getUserDetails(request);

      if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
           // 비밀번호가 일치하지 않다면
           throw new BadCredentialsException(&quot;비밀번호가 일치하지 않아!(T_T)&quot;);
        }
        Authentication authentication = authenticateHandler.authenticate(request);
        String accessToken = jwtProvider.createAccessToken(authentication);
        return new TokenDTO(accessToken);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 난 공부중이므로 AuthenticationManager을 사용해보고 싶다.&lt;/p&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️⃣ UserDetailsService 구현 클래스 생성&lt;/p&gt;
&lt;pre id=&quot;code_1765863507997&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    public final JdbcTemplate jdbcTemplate;

    @Override
    public UserDetails loadUserByUsername(String userId) {
        String sql = &quot;SELECT * FROM dalmuri.\&quot;USER\&quot; WHERE \&quot;USER_ID\&quot; = ?&quot;; // 여기 에러는 무시 가능
        UserDetailsImplDTO userImpl = new UserDetailsImplDTO();

        try {
            UserDTO user = jdbcTemplate.queryForObject(
                    sql,
                    new Object[]{userId},
                    (rs, rowNum) -&amp;gt; {   // Mapper
                        UserDTO u = new UserDTO();
                        u.setUserCd(rs.getString(&quot;USER_CD&quot;));
                        u.setUserId(rs.getString(&quot;USER_ID&quot;));
                        u.setUserNm(rs.getString(&quot;USER_NM&quot;));
                        u.setPassword(rs.getString(&quot;PASSWORD&quot;));
                        u.setBgColor(rs.getString(&quot;BG_COLOR&quot;));
                        u.setTextColor(rs.getString(&quot;TEXT_COLOR&quot;));
                        u.setInsertDatetime(rs.getString(&quot;INSERT_DATETIME&quot;));
                        return u;
                    }
            );
            if (user == null) {
                throw new UsernameNotFoundException(&quot;사용자 정보 없음: &quot; + userId);
            }
            userImpl.setUserCd(user.getUserCd());
            userImpl.setUserId(user.getUserId());
            userImpl.setUserNm(user.getUserNm());
            userImpl.setPassword(user.getPassword());
            userImpl.setBgColor(user.getBgColor());
            userImpl.setTextColor(user.getTextColor());
            userImpl.setInsertDatetime(user.getInsertDatetime());
            return userImpl;
        } catch (Exception e) {
            e.printStackTrace();
            throw new UsernameNotFoundException(&quot;사용자 정보 없음: &quot; + userId);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 기존 AuthService.java에 있던 DB호출로직은 삭제&lt;/p&gt;
&lt;pre id=&quot;code_1765863550431&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//    private UserDetailsImpl getUserDetails (LoginRequestDTO request) {
//        String sql = &quot;SELECT * FROM dalmuri.\&quot;USER\&quot; WHERE \&quot;USER_ID\&quot; = ?&quot;; // 여기 에러는 무시 가능
//
//        User user = jdbcTemplate.queryForObject(
//                sql,
//                new Object[]{request.getUserId()},
//                (rs, rowNum) -&amp;gt; {   // Mapper
//                    User u = new User();
//                    u.setUserCd(rs.getString(&quot;USER_CD&quot;));
//                    u.setUserId(rs.getString(&quot;USER_ID&quot;));
//                    u.setUserNm(rs.getString(&quot;USER_NM&quot;));
//                    u.setPassword(rs.getString(&quot;PASSWORD&quot;));
//                    u.setBgColor(rs.getString(&quot;BG_COLOR&quot;));
//                    u.setTextColor(rs.getString(&quot;TEXT_COLOR&quot;));
//                    u.setInsertDatetime(rs.getString(&quot;INSERT_DATETIME&quot;));
//                    return u;
//                }
//        );
//
//        if (user == null) {
//            throw new UsernameNotFoundException(&quot;사용자 정보 없음: &quot; + request.getUserId());
//        }
//
//        return new UserDetailsImpl(user);
//    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 삭제하냐면, AuthService에서 같이 생성했기 때문에 getUserDetails 메소드는 UserDetailsService 인터페이스를 상속받지 않는다(AuthService가 UserDetailsService를 상속받지 않음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ User을 UserDetails로 변환&lt;/p&gt;
&lt;pre id=&quot;code_1765864883621&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
@ToString
public class UserDetailsDTO implements UserDetails {

    private String userCd;
    private String userId;
    private String userNm;
    private String password;

    private String bgColor;
    private String textColor;
    private String insertDatetime;

    /*
    * 1. Lombok 어노테이션을 사용해도 UserDetails에서 요구하는 인터페이스는 직접 작성해야 한다.
    * */

    private final String role = &quot;ROLE_USER&quot;;

    @Override
    public String getUsername() {
        return userId;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        // 고정 반환
        return List.of(new SimpleGrantedAuthority(this.role));
    }

    /**
     * 형식적인 요소들 : true로 고정
     * */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 만들었던 UserDTO&lt;/p&gt;
&lt;pre id=&quot;code_1765864911438&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {

    private String userCd;
    private String userId;
    private String userNm;
    private String password;

    // 있으나 없으나
    private String bgColor;
    private String textColor;
    private String insertDatetime;

}&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;3️⃣ AuthService.java 로직 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 로직&lt;/p&gt;
&lt;pre id=&quot;code_1765860561365&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AuthService {

    private final JdbcTemplate jdbcTemplate;
    private final AuthenticateHandler authenticateHandler;
    private final JwtProvider jwtProvider;

    public TokenDTO login(LoginRequestDTO request) {
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        UserDetailsImpl userDetails = getUserDetails(request);

        if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
            // 비밀번호가 일치하지 않다면
            throw new BadCredentialsException(&quot;비밀번호가 일치하지 않아!(T_T)&quot;);
        }

        Authentication authentication = authenticateHandler.authenticate(request);

        String accessToken = jwtProvider.createAccessToken(authentication);

        return new TokenDTO(accessToken);
    }

    private UserDetailsImpl getUserDetails (LoginRequestDTO request) {
        String sql = &quot;SELECT * FROM dalmuri.\&quot;USER\&quot; WHERE \&quot;USER_ID\&quot; = ?&quot;; // 여기 에러는 무시 가능
        User user = jdbcTemplate.queryForObject(
                sql,
                new Object[]{request.getUserId()},
                (rs, rowNum) -&amp;gt; {   // Mapper
                    User u = new User();
                    u.setUserCd(rs.getString(&quot;USER_CD&quot;));
                    u.setUserId(rs.getString(&quot;USER_ID&quot;));
                    u.setUserNm(rs.getString(&quot;USER_NM&quot;));
                    u.setPassword(rs.getString(&quot;PASSWORD&quot;));
                    u.setBgColor(rs.getString(&quot;BG_COLOR&quot;));
                    u.setTextColor(rs.getString(&quot;TEXT_COLOR&quot;));
                    u.setInsertDatetime(rs.getString(&quot;INSERT_DATETIME&quot;));
                    return u;
                }
        );

        if (user == null) {
            throw new UsernameNotFoundException(&quot;사용자 정보 없음: &quot; + request.getUserId());
        }

        return new UserDetailsImpl(user);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 고친 로직&lt;/p&gt;
&lt;pre id=&quot;code_1765864994485&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {

    private final AuthenticateHandler authenticateHandler;
    private final JwtProvider jwtProvider;
    private final UserDetailsServiceImpl userDetailsServiceImpl;

    public TokenDTO login(LoginRequestDTO request) {
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        UserDetailsDTO userDetails = (UserDetailsDTO) userDetailsServiceImpl.loadUserByUsername(request.getUserId());

        if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
            // 비밀번호가 일치하지 않다면
            throw new BadCredentialsException(&quot;비밀번호가 일치하지 않아!(T_T)&quot;);
        }

        Authentication authentication = authenticateHandler.authenticate(request);

        String accessToken = jwtProvider.createAccessToken(authentication);

        return new TokenDTO(accessToken);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[참고 자료]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@corgi/Spring-Security-PasswordEncoder%EB%9E%80-4kkyw8gi&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@corgi/Spring-Security-PasswordEncoder%EB%9E%80-4kkyw8gi&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767146140051&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring Security] PasswordEncoder란?&quot; data-og-description=&quot;Spring Security 5.3.3 버전에서 지원하는 PasswordEncoder에 대해 간략히 알아봅니다.&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@corgi/Spring-Security-PasswordEncoder%EB%9E%80-4kkyw8gi&quot; data-og-url=&quot;https://velog.io/@corgi/Spring-Security-PasswordEncoder란-4kkyw8gi&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gYACD/hyZPQjjUfL/3OKuG0BBk4HOL0fRKP5pS0/img.png?width=850&amp;amp;height=478&amp;amp;face=0_0_850_478,https://scrap.kakaocdn.net/dn/cs9H0M/hyZPNz7POa/yS9ZLdlEI83WkGMeOuhOgk/img.png?width=850&amp;amp;height=478&amp;amp;face=0_0_850_478,https://scrap.kakaocdn.net/dn/ba4P3u/hyZQPC6oQ0/kP7H9pkWTn7sh3lQuwGxI0/img.png?width=850&amp;amp;height=478&amp;amp;face=0_0_850_478&quot;&gt;&lt;a href=&quot;https://velog.io/@corgi/Spring-Security-PasswordEncoder%EB%9E%80-4kkyw8gi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@corgi/Spring-Security-PasswordEncoder%EB%9E%80-4kkyw8gi&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gYACD/hyZPQjjUfL/3OKuG0BBk4HOL0fRKP5pS0/img.png?width=850&amp;amp;height=478&amp;amp;face=0_0_850_478,https://scrap.kakaocdn.net/dn/cs9H0M/hyZPNz7POa/yS9ZLdlEI83WkGMeOuhOgk/img.png?width=850&amp;amp;height=478&amp;amp;face=0_0_850_478,https://scrap.kakaocdn.net/dn/ba4P3u/hyZQPC6oQ0/kP7H9pkWTn7sh3lQuwGxI0/img.png?width=850&amp;amp;height=478&amp;amp;face=0_0_850_478');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Security] PasswordEncoder란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security 5.3.3 버전에서 지원하는 PasswordEncoder에 대해 간략히 알아봅니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://coding-start.tistory.com/153&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://coding-start.tistory.com/153&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767147029058&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring boot - Spring Security(스프링 시큐리티) 란? 완전 해결!&quot; data-og-description=&quot;오늘 포스팅할 내용은 Spring Security이다. 사실 필자는 머리가 나빠서 그런지 모르겠지만, 아무리 구글링을 통해 스프링 시큐리티를 검색해도 이렇다할 명쾌한 해답을 얻지 못했다. 대부분 이론적&quot; data-og-host=&quot;coding-start.tistory.com&quot; data-og-source-url=&quot;https://coding-start.tistory.com/153&quot; data-og-url=&quot;https://coding-start.tistory.com/153&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/twf97/hyZQ4mLTOB/hTrSFcePf0dZlwyzk4UnW1/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/fBGOh/hyZPRoXVLV/HhEVj2fDJTL5k3SeiD7VEk/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/beNZkD/hyZQYfM3eh/gGoaDK4gkAQdPBM4y2LHG0/img.png?width=1840&amp;amp;height=1348&amp;amp;face=0_0_1840_1348&quot;&gt;&lt;a href=&quot;https://coding-start.tistory.com/153&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://coding-start.tistory.com/153&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/twf97/hyZQ4mLTOB/hTrSFcePf0dZlwyzk4UnW1/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/fBGOh/hyZPRoXVLV/HhEVj2fDJTL5k3SeiD7VEk/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/beNZkD/hyZQYfM3eh/gGoaDK4gkAQdPBM4y2LHG0/img.png?width=1840&amp;amp;height=1348&amp;amp;face=0_0_1840_1348');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring boot - Spring Security(스프링 시큐리티) 란? 완전 해결!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;오늘 포스팅할 내용은 Spring Security이다. 사실 필자는 머리가 나빠서 그런지 모르겠지만, 아무리 구글링을 통해 스프링 시큐리티를 검색해도 이렇다할 명쾌한 해답을 얻지 못했다. 대부분 이론적&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;coding-start.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cz1JM4/dJMcaacROiy/mpN63IABsnq47nrOat8pB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cz1JM4/dJMcaacROiy/mpN63IABsnq47nrOat8pB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cz1JM4/dJMcaacROiy/mpN63IABsnq47nrOat8pB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcz1JM4%2FdJMcaacROiy%2FmpN63IABsnq47nrOat8pB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;300&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Backend ️/Java☕(Spring )</category>
      <category>authentication</category>
      <category>authorization</category>
      <category>filter</category>
      <category>jwt</category>
      <category>spring boot</category>
      <category>Spring Security</category>
      <category>스프링</category>
      <category>자바</category>
      <category>필터</category>
      <author>JanuDev</author>
      <guid isPermaLink="true">https://janudev.tistory.com/54</guid>
      <comments>https://janudev.tistory.com/54#entry54comment</comments>
      <pubDate>Wed, 31 Dec 2025 16:30:12 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] JWT 이용해서 로그인 기능 만들기 - 사용자 검증</title>
      <link>https://janudev.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;너무 길어져서 설명 따로 분리한다.&lt;/p&gt;
&lt;pre id=&quot;code_1767142964668&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {

    private final AuthenticateHandler authenticateHandler;
    private final JwtProvider jwtProvider;
    private final UserDetailsServiceImpl userDetailsServiceImpl;

    public TokenDTO login(LoginRequestDTO request) {
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        /*
        * (1) 사용자 검증(UserDetailsService)
        * */
        UserDetailsDTO userDetails = (UserDetailsDTO) userDetailsServiceImpl.loadUserByUsername(request.getUserId());

        /*
        * (2) 비밀번호 비교(PasswordEncoder)
        * */
        if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
            throw new BadCredentialsException(&quot;비밀번호가 일치하지 않아!(T_T)&quot;);
        }

        /*
        * (3) Authentication 객체 생성
        * */
        Authentication authentication = authenticateHandler.authenticate(request);

        // (4) JWT 발급
        String accessToken = jwtProvider.createAccessToken(authentication);

        // (5) 클라이언트에 토큰 전달
        return new TokenDTO(accessToken);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 첫번째 순서 설명!&lt;/p&gt;
&lt;pre id=&quot;code_1767142927184&quot; class=&quot;pf&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;UserDetailsDTO userDetails = (UserDetailsDTO) userDetailsServiceImpl.loadUserByUsername(request.getUserId());&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;(1)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;UserDetailsDTO&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 꺼낸 데이터는 일반적으로 날것 그대로의 정보이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 USER 테이블 = 현실 세계의 사용자 데이터이다.&lt;/p&gt;
&lt;p data-ke-size=&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;UserDetails = 스프링 시큐리티가 이해하는 '인증용 인간'이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-start=&quot;651&quot; data-end=&quot;675&quot;&gt;DB &amp;rarr; UserDTO (그냥 데이터)&lt;/li&gt;
&lt;li data-start=&quot;676&quot; data-end=&quot;712&quot;&gt;UserDTO &amp;rarr; UserDetailsDTO (인증용 객체)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 UserDetailsDTO는 UserDetails 인터페이스를 가져와서 구현하기 때문에 어차피 UserDTO와 합쳐서 사용하진 못한다.&lt;/p&gt;
&lt;pre id=&quot;code_1767142927184&quot; class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@Setter
@ToString
public class UserDetailsDTO implements UserDetails {

    private String userCd;
    private String userId;
    private String userNm;
    private String password;

    // 있으나 없으나
    private String bgColor;
    private String textColor;
    private String insertDatetime;

    /*
    * 1. Lombok 어노테이션을 사용해도 UserDetails에서 요구하는 인터페이스는 직접 작성해야 한다.
    * */

    private final String role = &quot;ROLE_USER&quot;;

    @Override
    public String getUsername() {
        return userId;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        // 고정 반환
        return List.of(new SimpleGrantedAuthority(this.role));
    }

    /**
     * 형식적인 요소들 : true로 고정
     * */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ public&amp;nbsp;class&amp;nbsp;UserDetailsDTO&amp;nbsp;implements&amp;nbsp;UserDetails&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDetails는 Spring Security가 사용자 정보를 다루기 위해 강제로 요구하는 인터페이스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인터페이스를 구현했다는건   &amp;ldquo;이 객체는 로그인한 사용자로 써도 된다&amp;rdquo;는 뜻.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 UserDTO와 분리한 이유.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ 필드부 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안에 직접 관여하는건 아니지만 각 프로젝트별로 필요한 User의 정보를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ 권한(role) 설계 - private final String role = &quot;ROLE_USER&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 권한 이름이 반드시 ROLE_로 시작해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-start=&quot;1489&quot; data-end=&quot;1499&quot;&gt;USER ❌&lt;/li&gt;
&lt;li data-start=&quot;1500&quot; data-end=&quot;1516&quot;&gt;ROLE_USER ⭕️&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 내 프로젝트의 경우 권한은 딱히 없어서... 그냥 ROLE_USER로 박아뒀다. 나중에 필요하면 바뀔수도...? 아직은 계획x&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4️⃣ 핵심 메소드&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getUsername() : Spring Security가 &quot;이 사용자의 아이디가 무엇인가&quot; 라고 물을 때 호출된다. 보통 userId를 반환하는게 정석(정답은 아님~)&lt;/li&gt;
&lt;li&gt;getPassword() : DB에서 저장된 암호화된 비밀번호를 가져온다. 인증 과정에서 PasswordEncoder.matches()에 사용됨&lt;/li&gt;
&lt;li&gt;getAuthorities() : &quot;이 사용자의 권한을 정의&quot;&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GrantedAuthority : 권한 인터페이스&lt;/li&gt;
&lt;li&gt;SimpleGrantedAuthority : 가장 기본의 구현체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5️⃣ 계정 상태 관련 메소드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 형식적인 요소들이기 때문에 true로 고정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;public boolean isAccountNonExpired() { return true; } : 계정 만료 여부&lt;/li&gt;
&lt;li&gt;public boolean isAccountNonLocked() { return true; } : 계정 잠김 여부. 로그인 실패 5회 정책만들 때 사용&lt;/li&gt;
&lt;li&gt;public boolean isCredentialsNonExpired() { return true; } : 비밀번호 만료 여부. 90일마다 변경 정책 때 사용&lt;/li&gt;
&lt;li&gt;public boolean isEnabled() { return true; } : 계정 활성화 여부. 탈퇴, 휴면 계정 처리할 때 사용&lt;/li&gt;
&lt;/ul&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;(2)&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;UserDetailsServiceImpl&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지피티의 고견으로는 이 부분이 Spring Security 인증의 &quot;심장&quot;부분이라 하신다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게까지 거창한 표현을 쓸줄이야...&lt;/p&gt;
&lt;pre id=&quot;code_1767142927186&quot; class=&quot;pf&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    public final JdbcTemplate jdbcTemplate;

    @Override
    public UserDetails loadUserByUsername(String userId) {
        String sql = &quot;SELECT * FROM \&quot;USER\&quot; WHERE \&quot;USER_ID\&quot; = ?&quot;; // 여기 에러는 무시 가능
        UserDetailsDTO userImpl = new UserDetailsDTO();

        try {
            UserDTO user = jdbcTemplate.queryForObject(
                    sql,
                    new Object[]{userId},
                    (rs, rowNum) -&amp;gt; {   // Mapper
                        UserDTO u = new UserDTO();
                        u.setUserCd(rs.getString(&quot;USER_CD&quot;));
                        u.setUserId(rs.getString(&quot;USER_ID&quot;));
                        u.setUserNm(rs.getString(&quot;USER_NM&quot;));
                        u.setPassword(rs.getString(&quot;PASSWORD&quot;));
                        u.setBgColor(rs.getString(&quot;BG_COLOR&quot;));
                        u.setTextColor(rs.getString(&quot;TEXT_COLOR&quot;));
                        u.setInsertDatetime(rs.getString(&quot;INSERT_DATETIME&quot;));
                        return u;
                    }
            );

            if (user == null) {
                throw new UsernameNotFoundException(&quot;사용자 정보 없음: &quot; + userId);
            }

            userImpl.setUserCd(user.getUserCd());
            userImpl.setUserId(user.getUserId());
            userImpl.setUserNm(user.getUserNm());
            userImpl.setPassword(user.getPassword());
            userImpl.setBgColor(user.getBgColor());
            userImpl.setTextColor(user.getTextColor());
            userImpl.setInsertDatetime(user.getInsertDatetime());
            return userImpl;

        } catch (Exception e) {
            e.printStackTrace();
            throw new UsernameNotFoundException(&quot;사용자 정보 없음: &quot; + userId);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ public class UserDetailsServiceImpl implements UserDetailsService&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDetailsService 또한 Spring Security가 로그인 시 무조건 호출하는 서비스이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 아이디 + 비밀번호 입력&lt;/li&gt;
&lt;li&gt;Spring Security가 UserDetailsService를 찾음&lt;/li&gt;
&lt;li&gt;loadUserByUsername(username)을 자동으로 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 개발자가 따로 호출하지 않아도 자동으로 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 지금 implement한 이유는 그 안의 loadUserByUsername을 오버라이드해서 사용할려고.&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;2️⃣ JDBCTemplate 선택 이유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBCTemplate는 동기 처리가 가능하고, 예외 흐름이 명확하며, 트랜잭션과 디버깅이 쉽다. 이거 사용 안하면 Supabase SDK를 사용하는 수밖에 없는데, 그거 설치하고 세팅하기까지 또 엄청 오래 걸릴것이 당연지사, Kotlin 비동기식 + SDK 업데이트도 필요하기 때문에 이것저것 귀찮음 이슈로 JDBCTemplate를 선택했다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ public UserDetails loadUserByUsername&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;절대로 임의의 메소드가 아니고, UserDetailsService에 내장되어 있는 메소드이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 내부에서 AuthenticationManager &amp;rarr; UserDetailsService.loadUserByUsername()을 실행한다.&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;4️⃣ SQL작성문&lt;/p&gt;
&lt;pre id=&quot;code_1767142927187&quot; class=&quot;lsl&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;String sql = &quot;SELECT * FROM \&quot;USER\&quot; WHERE \&quot;USER_ID\&quot; = ? &quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 큰따옴표 &quot;USER&quot;로 작성해야 하므로(PostgreSQL을 사용함, 테이블명 &amp;amp; 컬럼명을 대문자로 함) 이스케이프 문자 \를 &quot;앞에 반드시 사용해준다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;35&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eMQImi/dJMcad1AGSi/WOsXitwkfKLYX9fOvnqTlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eMQImi/dJMcad1AGSi/WOsXitwkfKLYX9fOvnqTlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eMQImi/dJMcad1AGSi/WOsXitwkfKLYX9fOvnqTlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeMQImi%2FdJMcad1AGSi%2FWOsXitwkfKLYX9fOvnqTlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;366&quot; height=&quot;35&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;35&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 빨간색 표시와 함께 에러 문구가 뜨긴 하는데, sql은 잘도 돌아간다. 이게 싫으면 DB설계 때 소문자로 하던가ㅎ&lt;/p&gt;
&lt;pre id=&quot;code_1767142927188&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;jdbcTemplate.queryForObject(sql, param[], RowMapper)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queryForObject: 데이터베이스 쿼리를 실행하여, 단 하나의 객체를 조회할 때 사용하는 메소드. 단건 조회에 최적합. 결과가 0행이거나 2행 이상일 경우 예외 반환, 정상인 경우 UserDTO 객체 하나 반환&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫번째 인자 : 실행할 SQL. ?는 아직 값이 없는 자리&lt;/li&gt;
&lt;li&gt;두번째 인자 : 파라미터 배열. SQL의 첫번째 ?에 이 배열의 첫번째 값인 userId를 넣어달라는 뜻&lt;/li&gt;
&lt;li&gt;세번째 인자 : 람다식 RowMapper&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(rs, rowNum) -&amp;gt; {...} : RowMapper의 인터페이스 구현. 원래 형태는&lt;/p&gt;
&lt;pre id=&quot;code_1767142927188&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public UserDTO mapRow(ResultSet rs, int rowNum)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rs(ResultSet) : DB에서 조회한 현재 행. 커서가 이미 해당 행에 위치해 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rowNum : 몇번째 행인지(0부터 시작), query()에서 여러 행 처리할 때 유용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다식 내부에선 UserDTO를 반환하는 작업을 할것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1767142927188&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;(rs, rowNum) -&amp;gt; {   
    UserDTO u = new UserDTO();
    u.setUserCd(rs.getString(&quot;USER_CD&quot;));
    u.setUserId(rs.getString(&quot;USER_ID&quot;));
    u.setUserNm(rs.getString(&quot;USER_NM&quot;));
    u.setPassword(rs.getString(&quot;PASSWORD&quot;));
    u.setBgColor(rs.getString(&quot;BG_COLOR&quot;));
    u.setTextColor(rs.getString(&quot;TEXT_COLOR&quot;));
    u.setInsertDatetime(rs.getString(&quot;INSERT_DATETIME&quot;));
    return u;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그니까 사실 이건,&lt;/p&gt;
&lt;pre id=&quot;code_1767142927188&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;new RowMapper&amp;lt;UserDTO&amp;gt;() {
    @Override
    public UserDTO mapRow(ResultSet rs, int rowNum) throws SQLException {
        UserDTO u = new UserDTO();
        u.setUserCd(rs.getString(&quot;USER_CD&quot;));
        u.setUserId(rs.getString(&quot;USER_ID&quot;));
        u.setUserNm(rs.getString(&quot;USER_NM&quot;));
        u.setPassword(rs.getString(&quot;PASSWORD&quot;));
        u.setBgColor(rs.getString(&quot;BG_COLOR&quot;));
        u.setTextColor(rs.getString(&quot;TEXT_COLOR&quot;));
        u.setInsertDatetime(rs.getString(&quot;INSERT_DATETIME&quot;));
        return u;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거랑 똑같은 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다에 대한 설명은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://hstory0208.tistory.com/entry/Java%EC%9E%90%EB%B0%94-%EB%9E%8C%EB%8B%A4%EC%8B%9DLambda%EC%9D%B4%EB%9E%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%82%AC%EC%9A%A9%EB%B2%95&quot;&gt;여기에...&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5️⃣ 객체 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL로부터 반환된 객체 값을 userDetailsDTO에 넣어서 원래 메소드 UserDetailsDTO userDetails로 반환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RWcyb/dJMcabW9tz3/S7gV7SdJsILtlHErzhFcH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RWcyb/dJMcabW9tz3/S7gV7SdJsILtlHErzhFcH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RWcyb/dJMcabW9tz3/S7gV7SdJsILtlHErzhFcH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRWcyb%2FdJMcabW9tz3%2FS7gV7SdJsILtlHErzhFcH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;300&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[참고 자료]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://programmer93.tistory.com/68&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://programmer93.tistory.com/68&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767143225121&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring Security UserDetails, UserDetailsService 란? - 삽질중인 개발자&quot; data-og-description=&quot;Spring Security - UserDetails , UserDetailsService UserDetails 란? Spring Security에서 사용자의 정보를 담는 인터페이스이다. Spring Security에서 사용자의 정보를 불러오기 위해서 구현해야 하는 인터페이스로 기본&quot; data-og-host=&quot;programmer93.tistory.com&quot; data-og-source-url=&quot;https://programmer93.tistory.com/68&quot; data-og-url=&quot;https://programmer93.tistory.com/68&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/j4PJV/hyZRcyldBz/OF6nwrgZvhThwVWbtZwFd1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bCysTh/hyZQsv1AZT/PUw9h03uKMKs8tdafg92k0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bqoyqo/hyZQwrEZH2/VeLvZqcAwFtik37BKbX8m0/img.png?width=264&amp;amp;height=200&amp;amp;face=0_0_264_200&quot;&gt;&lt;a href=&quot;https://programmer93.tistory.com/68&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://programmer93.tistory.com/68&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/j4PJV/hyZRcyldBz/OF6nwrgZvhThwVWbtZwFd1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bCysTh/hyZQsv1AZT/PUw9h03uKMKs8tdafg92k0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bqoyqo/hyZQwrEZH2/VeLvZqcAwFtik37BKbX8m0/img.png?width=264&amp;amp;height=200&amp;amp;face=0_0_264_200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security UserDetails, UserDetailsService 란? - 삽질중인 개발자&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security - UserDetails , UserDetailsService UserDetails 란? Spring Security에서 사용자의 정보를 담는 인터페이스이다. Spring Security에서 사용자의 정보를 불러오기 위해서 구현해야 하는 인터페이스로 기본&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmer93.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@cyseok123/%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0%EC%97%90-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@cyseok123/%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0%EC%97%90-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767143248276&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring] Spring-Security / UserDetails와 UserDetailsService 커스터마이징&quot; data-og-description=&quot;UserDetails와 UserDetailsService 를 이용한 스프링 시큐리티 커스터마이징&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@cyseok123/%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0%EC%97%90-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@cyseok123/시큐리티에-추가하기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/h5A89/hyZQspfJmZ/5mwoTyZgKg7LrApDGMebW0/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://velog.io/@cyseok123/%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0%EC%97%90-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@cyseok123/%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0%EC%97%90-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/h5A89/hyZQspfJmZ/5mwoTyZgKg7LrApDGMebW0/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring] Spring-Security / UserDetails와 UserDetailsService 커스터마이징&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;UserDetails와 UserDetailsService 를 이용한 스프링 시큐리티 커스터마이징&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend ️/Java☕(Spring )</category>
      <category>Authenticatoin</category>
      <category>jwt</category>
      <category>Spring Security</category>
      <category>UserDetails</category>
      <category>UserDetailsService</category>
      <author>JanuDev</author>
      <guid isPermaLink="true">https://janudev.tistory.com/53</guid>
      <comments>https://janudev.tistory.com/53#entry53comment</comments>
      <pubDate>Wed, 31 Dec 2025 10:07:50 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring Security 맛보기</title>
      <link>https://janudev.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;미루고 미루던 Spring Security를 슬슬 공부할 때가 된 듯 하다.... 그날이 왔도다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAQfYR/dJMcafkOUqe/ymHTZrpYohxD4HPxLan7uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAQfYR/dJMcafkOUqe/ymHTZrpYohxD4HPxLan7uk/img.png&quot; data-alt=&quot;Spring 흐름에 대한 간단한 설명&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAQfYR/dJMcafkOUqe/ymHTZrpYohxD4HPxLan7uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAQfYR%2FdJMcafkOUqe%2FymHTZrpYohxD4HPxLan7uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;886&quot; height=&quot;377&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;377&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring 흐름에 대한 간단한 설명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdDB3V/dJMcadtI4I6/55QD9CHWjWdarIIycvAj0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdDB3V/dJMcadtI4I6/55QD9CHWjWdarIIycvAj0K/img.png&quot; data-alt=&quot;Spring 흐름에 대한 자세한 설명&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdDB3V/dJMcadtI4I6/55QD9CHWjWdarIIycvAj0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdDB3V%2FdJMcadtI4I6%2F55QD9CHWjWdarIIycvAj0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;680&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring 흐름에 대한 자세한 설명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Spring Security란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 Spring 기반의 애플리케이션의 보안을 담당하는 스프링 하위 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인, 보안을 위한 인증 &amp;bull; 인가들을 개발하기 위해선 Spring Security를 많이 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Spring Security 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security의 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1765375699810&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;요청(Request) &amp;rarr; 필터(Filter) &amp;rarr; 인증(Authentication) &amp;rarr; 인가(Authorization)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 요청(Request)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 요청(Request)란 클라이언트가 서버로 보내는 모든 HTTP 요청을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 한 번 인증했다고 해서 계속 로그인 상태임을 기억하지 않고, &lt;span style=&quot;color: #0593d3;&quot;&gt;기억력이 없는 존재(Stateless)&lt;/span&gt;로 설계된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 요청이 들어올 때 마다 &quot;이 요청을 보낸 사람이 아까 그 사용자라는 증거가 어딨지&quot; 라는 생각을 하는데, 그&lt;span style=&quot;color: #0593d3;&quot;&gt; 증거가 바로 JWT 토큰이고, 그걸 검사하는 것이 JWTFilter&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRUD와 같은 여러 RestfulAPI 요청이 들어올 때 마다, Authorization 헤더에 있는 JWT를 JWTFilter가 매번 확인한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 필터(Filter)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oBHCF/dJMcafkOVhz/ZPbgRZdw1zZcr7XLAEDMk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oBHCF/dJMcafkOVhz/ZPbgRZdw1zZcr7XLAEDMk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oBHCF/dJMcafkOVhz/ZPbgRZdw1zZcr7XLAEDMk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoBHCF%2FdJMcafkOVhz%2FZPbgRZdw1zZcr7XLAEDMk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;406&quot; height=&quot;340&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 들어오면 필터 체인(Filter Chain)이 여러 필터들을 나열해서, 요청이 들어올 때 마다 이 필터들을 줄줄이 검사하게 된다.&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;span style=&quot;color: #ef5369;&quot;&gt;UsernamePasswordAuthenticationFilter&lt;/span&gt;(일반 로그인) : 폼 기반 인증 처리 - LoginRequest와 같은 ID/PW 요청을 처리하고 Authentication 객체를 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;JWTAuthenticationFilter / JWTAuthorizationFilter&lt;/span&gt;(JWT 로그인) : JWT 기반 인증 처리 - 요청 헤더의 JWT 토큰을 추출하여 사용자를 인증한다(사용자가 구현하는 필터).&lt;/li&gt;
&lt;li&gt;SecurityContextPersistenceFilter : 인증 정보 유지 - 인증 성공 후 SecurityContext에 Authentication 객체를 저장하거나 조회한다. 요청이 시작될 때 이전에 저장된 SecurityContext가 있다면 읽어서 불러오고(JWT에선 보통 비활성화됨) 요청이 끝나면 SecurityContext를 clear(삭제)한다.&lt;/li&gt;
&lt;li&gt;ExceptionTranslationFilter : 예외 처리 - 인증 및 인가 예외를 처리하고 적절한 HTTP 응답 코드를 반환한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;FilterSecurityInterceptor&lt;/span&gt;(마지막 권한 체크) : 최종 인가 처리 - 리소스 접근 전 마지막으로 권한(Authority)을 확인하여 인가(Authorization)를 결정한다. - &lt;span style=&quot;color: #ee2323;&quot;&gt;일반 Interceptor과 헷갈리지 말것.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Persistence : 지속성, 끈기&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;SecurityContext&lt;/span&gt; :&amp;nbsp; 스프링 시큐리티에서 &quot;지금 이 요청을 보낸 사람에 대한 인증(로그인)정보가 들어 있는 작은 가방&quot; 같은 느낌. 스프링에서 JWT같은 인증 과정을 걸쳐서 &quot;인증된 사용자&quot;임이 확인이 되면, 그 인증 결과를 SecurityContext에 보관한다. 이 SecurityContext 안에 들어 있는 정보는 Authentication 객체이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Authentication 객체 : 인증의 결과를 담고 있는 핵심 객체. 누가 요청했는지(username), 그 사람의 권한(ROLE_USER, ROLE_ADMIN), 인증이 됬는지(Boolean) 이 들어있다.&lt;/li&gt;
&lt;li&gt;Authentication 객체의 역할 - GPT피셜 &lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;1. 사용자 정보 (Principal &amp;amp; Credentials)&lt;/b&gt;&lt;br /&gt;Principal (주체):&amp;nbsp;현재 시스템에 접근하려는&amp;nbsp;사용자를 나타냅니다. 보통 사용자 이름(username), 사용자 ID, 또는 사용자 상세 정보(UserDetails) 객체 자체가 될 수 있습니다.&lt;br /&gt;Credentials (자격 증명):&amp;nbsp;사용자의&amp;nbsp;비밀번호나 토큰과 같이 사용자가 자신이 주장하는 사람임을 증명하는 데 사용되는 정보입니다. 인증이 완료된 후에는 보안을 위해 이 정보는 보통 지워집니다.&lt;br /&gt;&lt;b&gt;2. 인증 상태 (Authenticated)&lt;/b&gt;&lt;br /&gt;isAuthenticated() 메서드를 통해 이 객체가 나타내는 사용자가&amp;nbsp;인증되었는지(true)&amp;nbsp;또는&amp;nbsp;인증 전인지(false)&amp;nbsp;상태를 나타냅니다.&lt;br /&gt;&lt;b&gt;3. 권한 정보 (Authorities)&lt;/b&gt;&lt;br /&gt;인증된 사용자가 가진&amp;nbsp;권한 목록(GrantedAuthority)을 담고 있습니다. 예를 들어, ROLE_ADMIN, ROLE_USER 등의 정보를 담고 있어, 이후 인가(Authorization) 단계에서 사용자가 특정 리소스에 접근할 수 있는지 판단하는 근거가 됩니다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터가 차례 차례 요청을 넘기는데, 필터 중간에 인증이 완료된다면 뒤의 필터들은 인증이 완료된 유저 정보(SecurityContext)를 그대로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 인증이 완료됬다면 SecurityContext 생성 &amp;amp; Authentication 객체가 저장된다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(3) 인증(Authentication)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1) AuthenticationManager가 진짜로 로그인을 검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 시도(아이디/비밀번호 인증)가 들어오면 AuthenticationManager가 등장에서 다음을 수행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유저 정보(loadUserByUsername) 가져오기&lt;/li&gt;
&lt;li&gt;비밀번호 확인&lt;/li&gt;
&lt;li&gt;성공하면 Authentication 객체 생성&lt;/li&gt;
&lt;li&gt;SecurityContext에 저장&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size18&quot;&gt;2) SecurityContext에 사용자 정보 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecurityContext는 &quot;이번 요청에서 인증된 유저 정보 저장소&quot;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증에 성공하면 Authentication 객체를 저장&lt;/li&gt;
&lt;li&gt;컨트롤러에서 @AuthenticationPrincipal 같은 것을 쓰면 그 정보를 읽을 수 있다.&lt;/li&gt;
&lt;li&gt;요청이 끝나면 사라진다.&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;+ 내 코드를 바탕으로 한 Spring Security이의 흐름&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 136px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.0775%;&quot;&gt;단계&lt;/td&gt;
&lt;td style=&quot;width: 21.1899%;&quot;&gt;주체&lt;/td&gt;
&lt;td style=&quot;width: 30.3993%;&quot;&gt;동작&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%;&quot;&gt;1. 요청 생성&lt;/td&gt;
&lt;td style=&quot;width: 21.1899%; height: 17px;&quot;&gt;Client&lt;/td&gt;
&lt;td style=&quot;width: 30.3993%; height: 17px;&quot;&gt;사용자 이름과 비밀번호를 담아 로그인 요청(/login)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;LoginRequestDTO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%;&quot;&gt;2. 인증 요청&lt;/td&gt;
&lt;td style=&quot;width: 21.1899%; height: 17px;&quot;&gt;AuthService&lt;/td&gt;
&lt;td style=&quot;width: 30.3993%; height: 17px;&quot;&gt;LoginRequest를 기반을 인증 전 토큰(UsernamePasswordAuthenticatoinToken) 생성&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;Authentication 객체(Unauthenticated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%;&quot;&gt;3. 인증 위임&lt;/td&gt;
&lt;td style=&quot;width: 21.1899%; height: 17px;&quot;&gt;LoginHandler&lt;/td&gt;
&lt;td style=&quot;width: 30.3993%; height: 17px;&quot;&gt;토큰을 AuthenticationManager에게 전달&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%;&quot;&gt;4. 인증 수행&lt;/td&gt;
&lt;td style=&quot;width: 21.1899%; height: 17px;&quot;&gt;AuthenticationManager&lt;/td&gt;
&lt;td style=&quot;width: 30.3993%; height: 17px;&quot;&gt;요청을 처리할 수 있는 AuthenticationProvider에게 위임&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%;&quot;&gt;5. 사용자 로드&lt;/td&gt;
&lt;td style=&quot;width: 21.1899%; height: 17px;&quot;&gt;AuthenticationProvider&lt;/td&gt;
&lt;td style=&quot;width: 30.3993%; height: 17px;&quot;&gt;UserDetailService를 통해 DB에서 사용자 정보(UserDetails) 로드&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%;&quot;&gt;6. 검증&lt;/td&gt;
&lt;td style=&quot;width: 21.1899%; height: 17px;&quot;&gt;AuthenticationProvider&lt;/td&gt;
&lt;td style=&quot;width: 30.3993%; height: 17px;&quot;&gt;PasswordEncoder로 비밀번호 비교&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%;&quot;&gt;7. 성공, 실패&lt;/td&gt;
&lt;td style=&quot;width: 21.1899%; height: 17px;&quot;&gt;AuthenticationProvider&lt;/td&gt;
&lt;td style=&quot;width: 30.3993%; height: 17px;&quot;&gt;성공 시 권한이 담긴 인증 완료 토큰 반환&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;Authentication 객체(Authenticated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%;&quot;&gt;7. 컨텍스트 저장&lt;/td&gt;
&lt;td style=&quot;width: 21.1899%; height: 17px;&quot;&gt;AuthService&lt;/td&gt;
&lt;td style=&quot;width: 30.3993%; height: 17px;&quot;&gt;인증 완료된 Authentication 객체를 SecurityContextHandler에 저장&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;인증 정보 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(4) 인가(Authorizatoin) - 권한 체크&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터 체인의 마지막 쯤에서 FilterSecurityInterceptor가 &quot;이 유저가 이 URL에 접근할 권한이 있는가?&quot;를 체크한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1576&quot; data-start=&quot;1525&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1550&quot; data-start=&quot;1525&quot;&gt;ROLE_USER는 /mypage 가능&lt;/li&gt;
&lt;li data-end=&quot;1576&quot; data-start=&quot;1551&quot;&gt;ROLE_ADMIN은 /admin 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1600&quot; data-start=&quot;1578&quot; data-ke-size=&quot;size16&quot;&gt;권한이 없으면 403 Forbidden.&lt;/p&gt;
&lt;blockquote data-end=&quot;1600&quot; data-start=&quot;1578&quot; data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;주의! &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;i&gt;&lt;b&gt;FilterSecurityInterceptor과 일반 HandlerInterceptor를 헷갈리지 말것.&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&amp;bull; FilterSecurityInterceptor : 인증 및 인가 단계에서 실행 및 완료&lt;br /&gt;&amp;bull; 일반 HandlerInterceptor : DispatcherServlet이 핸들러를 찾은 직후, 컨트롤러 실행 '직전'에 동작함&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/erFOXZ/dJMcahCVSQR/o15jkkfhKrPcQKlZCmRffk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/erFOXZ/dJMcahCVSQR/o15jkkfhKrPcQKlZCmRffk/img.png&quot; data-alt=&quot;이렇게 이해하면 되겠다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/erFOXZ/dJMcahCVSQR/o15jkkfhKrPcQKlZCmRffk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FerFOXZ%2FdJMcahCVSQR%2Fo15jkkfhKrPcQKlZCmRffk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;644&quot; height=&quot;364&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이렇게 이해하면 되겠다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-end=&quot;1600&quot; data-start=&quot;1578&quot; data-ke-size=&quot;size23&quot;&gt;3. Spring MVC 전체 요청 흐름&lt;/h3&gt;
&lt;pre id=&quot;code_1765378437332&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Client &amp;rarr; Filter &amp;rarr; DispatcherServlet &amp;rarr; Interceptor &amp;rarr; Controller&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞다. 이게 내가 원래 공부했던 Spring MVC 패턴이다. 여기에 대입해서 설명할거다.&lt;/p&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;li&gt;&quot;서블릿 필터&quot;가 먼저 실행됨&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #9feec3;&quot;&gt; &amp;larr; 여기에 위에서 설명한 Spring Security Filter chain들 있어요 &lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;DispatcherServlet이 요청을 잡음&lt;/li&gt;
&lt;li&gt;Interceptor가 동작&lt;/li&gt;
&lt;li&gt;Controller 실행&lt;/li&gt;
&lt;li&gt;응답 반환...&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;즉 앞서 말한 Spring Security의 흐름은 &quot;Filer Chain&quot;의 내부 모습을 확대해서 본 것이라 봐도 무방하다.&lt;/span&gt;&lt;/b&gt;&lt;/u&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 style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://www.notion.so/Intercepor-10b465d475ae8034bbe7fc24b166f5c7&quot;&gt;Interceptor에 대한 정리에 대한 글&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DispatcherServlet이란? (복습 겸)&lt;/b&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beQb8B/dJMcacO90UJ/aKpvH8wDbSq3QrkpkZAFkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beQb8B/dJMcacO90UJ/aKpvH8wDbSq3QrkpkZAFkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beQb8B/dJMcacO90UJ/aKpvH8wDbSq3QrkpkZAFkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeQb8B%2FdJMcacO90UJ%2FaKpvH8wDbSq3QrkpkZAFkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;670&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;웹 요청이 들어오면 Spring MVC는 그걸 어느 컨트롤러에서 처리해야 하는지 연결해야 하는데, 그 흐름을 조종하는 친구가 DispatcherServlet이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1432&quot; data-start=&quot;1297&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1326&quot; data-start=&quot;1297&quot;&gt;&lt;b&gt;Filter&lt;/b&gt; &amp;rarr; 학교 정문 경비 아저씨&lt;/li&gt;
&lt;li data-end=&quot;1390&quot; data-start=&quot;1327&quot;&gt;&lt;b&gt;DispatcherServlet&lt;/b&gt; &amp;rarr; 교무실에서 &amp;ldquo;이 학생 어디 반으로 보내야 하지?&amp;rdquo; 정리하는 교사&lt;/li&gt;
&lt;li data-end=&quot;1432&quot; data-start=&quot;1391&quot;&gt;&lt;b&gt;Controller&lt;/b&gt; &amp;rarr; 실제 수업(비즈니스 로직)을 담당하는 반&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1472&quot; data-start=&quot;1434&quot; data-ke-size=&quot;size16&quot;&gt;MVC 요청 흐름에서 DispatcherServlet은 중심 허브이다.&lt;/p&gt;
&lt;p data-end=&quot;1472&quot; data-start=&quot;1434&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1472&quot; data-start=&quot;1434&quot; data-ke-size=&quot;size16&quot;&gt;DispatcherServlet의 역할은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1472&quot; data-start=&quot;1434&quot;&gt;URL을 보고 어떤 Controller을 호출해야 하는지 결정&lt;/li&gt;
&lt;li data-end=&quot;1472&quot; data-start=&quot;1434&quot;&gt;Controller 실행&lt;/li&gt;
&lt;li data-end=&quot;1472&quot; data-start=&quot;1434&quot;&gt;Controller가 반환 값을 ViewResolver로 전달&lt;/li&gt;
&lt;li data-end=&quot;1472&quot; data-start=&quot;1434&quot;&gt;최종 JSON, HTML을 만들어서 응답 완성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Dispatch는 급파하다, 파견하다의 의미를 가지고 있으며 받은 요청을 어딘가로 빨리 보내는 서블릿이라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring이 없는 Java 런타임은 컨트롤러가 존재하지 않기 때문에 서블릿 객체를 생성하고 그것을 web.xml에 일일히 다 등록해야 했는데, DispatcherServlet은 이러한 단점을 해결해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/Servlet-ac3c4a90ee954c3c813568e160a37783#05fbd0a223bd4059b2e30089da09ded4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;서블릿에 대한 개념은 여기 참조&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[참고 자료]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/76&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mangkyu.tistory.com/76&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765375455792&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;[SpringBoot] Spring Security란?&quot; data-og-description=&quot;대부분의 시스템에서는 회원의 관리를 하고 있고, 그에 따른 인증(Authentication)과 인가(Authorization)에 대한 처리를 해주어야 한다. Spring에서는 Spring Security라는 별도의 프레임워크에서 관련된 기능&quot; data-og-host=&quot;mangkyu.tistory.com&quot; data-og-source-url=&quot;https://mangkyu.tistory.com/76&quot; data-og-url=&quot;https://mangkyu.tistory.com/76&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bqDqbF/hyZPlpTiUI/KCUDWhJsKJM6ujd0O6lcG0/img.png?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/ol8KU/hyZPaojubm/itxN1ysus5xi2ucA21fSYk/img.png?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/Inhkw/hyZPd6rGeq/bhOfpyEnokzfK72M8lnS50/img.png?width=1840&amp;amp;height=1348&amp;amp;face=0_0_1840_1348&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/76&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mangkyu.tistory.com/76&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bqDqbF/hyZPlpTiUI/KCUDWhJsKJM6ujd0O6lcG0/img.png?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/ol8KU/hyZPaojubm/itxN1ysus5xi2ucA21fSYk/img.png?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/Inhkw/hyZPd6rGeq/bhOfpyEnokzfK72M8lnS50/img.png?width=1840&amp;amp;height=1348&amp;amp;face=0_0_1840_1348');&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;[SpringBoot] Spring Security란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 시스템에서는 회원의 관리를 하고 있고, 그에 따른 인증(Authentication)과 인가(Authorization)에 대한 처리를 해주어야 한다. Spring에서는 Spring Security라는 별도의 프레임워크에서 관련된 기능&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mangkyu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://prao.tistory.com/entry/Spring-Security-Spring-Security%EC%99%80-JWT-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://prao.tistory.com/entry/Spring-Security-Spring-Security%EC%99%80-JWT-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765377126766&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring Security] Spring Security와 JWT 적용 과정&quot; data-og-description=&quot;사이드 프로젝트를 진행하면서 Spring Security와 JWT를 적용하여 회원 기능을 완성하는 역할을 맡게 되었다.JWT는 프로젝트에 적용시켜본 경험이 있으나 Spring Security는 처음이었고 Spring Security와 JWT&quot; data-og-host=&quot;prao.tistory.com&quot; data-og-source-url=&quot;https://prao.tistory.com/entry/Spring-Security-Spring-Security%EC%99%80-JWT-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95&quot; data-og-url=&quot;https://prao.tistory.com/entry/Spring-Security-Spring-Security%EC%99%80-JWT-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dmH0fu/hyZPlwFrbR/Ayv5gxisZG8PLNuKkDhrx1/img.png?width=546&amp;amp;height=370&amp;amp;face=0_0_546_370,https://scrap.kakaocdn.net/dn/1aO6X/hyZOIFtwK4/r5M3MoYkV9iSY5IR5u3fTk/img.png?width=546&amp;amp;height=370&amp;amp;face=0_0_546_370,https://scrap.kakaocdn.net/dn/cRQt8p/hyZPjZUhLd/dDJNBwlMBxFOs91cSyMRZk/img.png?width=2336&amp;amp;height=1656&amp;amp;face=0_0_2336_1656&quot;&gt;&lt;a href=&quot;https://prao.tistory.com/entry/Spring-Security-Spring-Security%EC%99%80-JWT-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prao.tistory.com/entry/Spring-Security-Spring-Security%EC%99%80-JWT-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dmH0fu/hyZPlwFrbR/Ayv5gxisZG8PLNuKkDhrx1/img.png?width=546&amp;amp;height=370&amp;amp;face=0_0_546_370,https://scrap.kakaocdn.net/dn/1aO6X/hyZOIFtwK4/r5M3MoYkV9iSY5IR5u3fTk/img.png?width=546&amp;amp;height=370&amp;amp;face=0_0_546_370,https://scrap.kakaocdn.net/dn/cRQt8p/hyZPjZUhLd/dDJNBwlMBxFOs91cSyMRZk/img.png?width=2336&amp;amp;height=1656&amp;amp;face=0_0_2336_1656');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Security] Spring Security와 JWT 적용 과정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트를 진행하면서 Spring Security와 JWT를 적용하여 회원 기능을 완성하는 역할을 맡게 되었다.JWT는 프로젝트에 적용시켜본 경험이 있으나 Spring Security는 처음이었고 Spring Security와 JWT&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prao.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jiwon.oopy.io/1cfde91b-5fd3-4851-9419-9045d971d2cf&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jiwon.oopy.io/1cfde91b-5fd3-4851-9419-9045d971d2cf&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765378134348&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;Spring Security 구조 이해하기&quot; data-og-description=&quot;Spring Security란?&quot; data-og-host=&quot;jiwon.oopy.io&quot; data-og-source-url=&quot;https://jiwon.oopy.io/1cfde91b-5fd3-4851-9419-9045d971d2cf&quot; data-og-url=&quot;https://jiwon.oopy.io/1cfde91b-5fd3-4851-9419-9045d971d2cf&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cJvaUf/hyZPbnhNin/FFGCvbApLNc1hKSkObZye1/img.jpg?width=1686&amp;amp;height=859&amp;amp;face=0_0_1686_859,https://scrap.kakaocdn.net/dn/t0fbK/hyZOOyS72c/Papoi9nTfcJMtziifrpTXk/img.jpg?width=1686&amp;amp;height=859&amp;amp;face=0_0_1686_859&quot;&gt;&lt;a href=&quot;https://jiwon.oopy.io/1cfde91b-5fd3-4851-9419-9045d971d2cf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jiwon.oopy.io/1cfde91b-5fd3-4851-9419-9045d971d2cf&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cJvaUf/hyZPbnhNin/FFGCvbApLNc1hKSkObZye1/img.jpg?width=1686&amp;amp;height=859&amp;amp;face=0_0_1686_859,https://scrap.kakaocdn.net/dn/t0fbK/hyZOOyS72c/Papoi9nTfcJMtziifrpTXk/img.jpg?width=1686&amp;amp;height=859&amp;amp;face=0_0_1686_859');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security 구조 이해하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security란?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jiwon.oopy.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@seculoper235/2.-DispatcherServlet-%EC%9D%B4%EB%9E%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@seculoper235/2.-DispatcherServlet-%EC%9D%B4%EB%9E%80&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765378788810&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;2. DispatcherServlet 이란?&quot; data-og-description=&quot;드디어 시작되는 DispatcherServlet!해당 클래스는 Servlet의 중심이자, Spring MVC의 중심이다.이것을 이해하는 것이 곧 Spring MVC를 이해하는 것과 같다고 할 수 있을 정도로 중요한 요소이다 : )그럼 한번 &quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@seculoper235/2.-DispatcherServlet-%EC%9D%B4%EB%9E%80&quot; data-og-url=&quot;https://velog.io/@seculoper235/2.-DispatcherServlet-이란&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/NgVAe/hyZOJqNTKY/C4KvDXmPPcEXaYfIVZGe00/img.png?width=1068&amp;amp;height=670&amp;amp;face=0_0_1068_670,https://scrap.kakaocdn.net/dn/DCenx/hyZO7SF0Ib/ltvgb4lAAf9vibegJZgvTK/img.png?width=1068&amp;amp;height=670&amp;amp;face=0_0_1068_670,https://scrap.kakaocdn.net/dn/cdPgg6/hyZPcNelE0/fGD5WRJgLSLACQW7Pi9ED0/img.png?width=1068&amp;amp;height=670&amp;amp;face=0_0_1068_670&quot;&gt;&lt;a href=&quot;https://velog.io/@seculoper235/2.-DispatcherServlet-%EC%9D%B4%EB%9E%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@seculoper235/2.-DispatcherServlet-%EC%9D%B4%EB%9E%80&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/NgVAe/hyZOJqNTKY/C4KvDXmPPcEXaYfIVZGe00/img.png?width=1068&amp;amp;height=670&amp;amp;face=0_0_1068_670,https://scrap.kakaocdn.net/dn/DCenx/hyZO7SF0Ib/ltvgb4lAAf9vibegJZgvTK/img.png?width=1068&amp;amp;height=670&amp;amp;face=0_0_1068_670,https://scrap.kakaocdn.net/dn/cdPgg6/hyZPcNelE0/fGD5WRJgLSLACQW7Pi9ED0/img.png?width=1068&amp;amp;height=670&amp;amp;face=0_0_1068_670');&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;2. DispatcherServlet 이란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;드디어 시작되는 DispatcherServlet!해당 클래스는 Servlet의 중심이자, Spring MVC의 중심이다.이것을 이해하는 것이 곧 Spring MVC를 이해하는 것과 같다고 할 수 있을 정도로 중요한 요소이다 : )그럼 한번&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MeCkT/dJMcaajvHHP/99CE3r1qExatEO0mnT0VYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MeCkT/dJMcaajvHHP/99CE3r1qExatEO0mnT0VYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MeCkT/dJMcaajvHHP/99CE3r1qExatEO0mnT0VYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMeCkT%2FdJMcaajvHHP%2F99CE3r1qExatEO0mnT0VYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;300&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Backend ️/Java☕(Spring )</category>
      <category>DispatcherServlet</category>
      <category>filter</category>
      <category>Filter Chain</category>
      <category>jwt</category>
      <category>JWT Authenticaton</category>
      <category>JWT Token</category>
      <category>spring</category>
      <category>spring mvc</category>
      <category>Spring Security</category>
      <category>필터</category>
      <author>JanuDev</author>
      <guid isPermaLink="true">https://janudev.tistory.com/52</guid>
      <comments>https://janudev.tistory.com/52#entry52comment</comments>
      <pubDate>Thu, 11 Dec 2025 00:04:22 +0900</pubDate>
    </item>
    <item>
      <title>티스토리에서 수식 쓰기</title>
      <link>https://janudev.tistory.com/51</link>
      <description>&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;size18&quot;&gt;1. 블로그 관리 &amp;gt; 스킨 편집 &amp;gt; html 편집 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccGSgM/dJMcaacJNxe/qhvatE73gUWTL1KWwh2kK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccGSgM/dJMcaacJNxe/qhvatE73gUWTL1KWwh2kK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccGSgM/dJMcaacJNxe/qhvatE73gUWTL1KWwh2kK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccGSgM%2FdJMcaacJNxe%2FqhvatE73gUWTL1KWwh2kK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;360&quot; height=&quot;168&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. &amp;lt;head&amp;gt;태그 안에 수식 관련 script 붙여넣기&lt;/p&gt;
&lt;pre id=&quot;code_1765352180995&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;https://polyfill.io/v3/polyfill.min.js?features=es6&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script id=&quot;MathJax-script&quot; async src=&quot;https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 이거 복붙해서 붙여넣기하고 저장하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d688Ed/dJMcai9DkRr/4JBPukvRxv17wLfOn0C1r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d688Ed/dJMcai9DkRr/4JBPukvRxv17wLfOn0C1r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d688Ed/dJMcai9DkRr/4JBPukvRxv17wLfOn0C1r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd688Ed%2FdJMcai9DkRr%2F4JBPukvRxv17wLfOn0C1r0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;581&quot; height=&quot;327&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저기에 붙여넣으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 수식 작성하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수식 작성하는데 도움 받을 수 있는 사이트는 여기다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://editor.codecogs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://editor.codecogs.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765351919988&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;Equation Editor for online mathematics - create, integrate and download&quot; data-og-description=&quot;Download svg gif png pdf emf 5 pt 9 pt 10 pt 12 pt 18 pt 20 pt 50 80 100 110 120 150 200 300 Transparent White Black Red Green Blue Inline Block WordPress phpBB Tiny Wiki url url encoded xml pre doxygen html latex Formatted string containing your Equation &quot; data-og-host=&quot;editor.codecogs.com&quot; data-og-source-url=&quot;https://editor.codecogs.com/&quot; data-og-url=&quot;https://editor.codecogs.com/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://editor.codecogs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://editor.codecogs.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Equation Editor for online mathematics - create, integrate and download&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Download svg gif png pdf emf 5 pt 9 pt 10 pt 12 pt 18 pt 20 pt 50 80 100 110 120 150 200 300 Transparent White Black Red Green Blue Inline Block WordPress phpBB Tiny Wiki url url encoded xml pre doxygen html latex Formatted string containing your Equation&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;editor.codecogs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;477&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7bYx6/dJMcaa4Sjkh/kPenYhvCkjQP1zxAKNTtuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7bYx6/dJMcaa4Sjkh/kPenYhvCkjQP1zxAKNTtuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7bYx6/dJMcaa4Sjkh/kPenYhvCkjQP1zxAKNTtuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7bYx6%2FdJMcaa4Sjkh%2FkPenYhvCkjQP1zxAKNTtuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;912&quot; height=&quot;477&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;477&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 작성해서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;LaTeX 문법을 통해서 수식을 작성&lt;/b&gt;&lt;/span&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1️⃣ 텍스트 사이에 수식을 작성할 경우&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1765353764579&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;\(여기 안에 수식을 써야 한다 \)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(여기 안에 수식을 써야 한다. a_x\)&lt;/p&gt;
&lt;p data-ke-size=&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;2️⃣ 인라인, 디스플레이 수식&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;텍스트와 함께 왼쪽 정렬에 따른다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1765353459108&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;수열의 $x$ 번째 항은 $a_x$ 이다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저렇게 단일 달러 기호로 감싼다 ($...$)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-path-to-node=&quot;4,7,1,0&quot;&gt;디스플레이 수식은 텍스트 줄에서 분리되어 독립적인 단락처럼 표시된다. 수식의 위아래로 공백이 생긴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-path-to-node=&quot;4,7,1,0&quot;&gt;페이지나 문서 영역의 중앙에 정렬된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1765353635662&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$$a_x$$&lt;/code&gt;&lt;/pre&gt;
&lt;div data-math=&quot;\text{$$a_x$$}&quot;&gt;$$a_x$$&lt;/div&gt;
&lt;div data-math=&quot;\text{$$a_x$$}&quot;&gt;이렇게 보인다.&amp;nbsp;&lt;/div&gt;
&lt;div data-math=&quot;\text{$$a_x$$}&quot;&gt;참고로 티스토리에선 인라인 수식은 인식이 안되는데 디스플레이 수식은 인식이 되는듯 하다. 그래서 &lt;b&gt;1️⃣&lt;/b&gt; 와 같은 방법으로 작성한다.&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-path-to-node=&quot;4,7,1,0&quot;&gt;3️⃣LaTeX 기본 문법&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 110px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 21px;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 21px;&quot;&gt;명령어&lt;/td&gt;
&lt;td style=&quot;width: 16.0466%; height: 21px;&quot;&gt;결과&lt;/td&gt;
&lt;td style=&quot;width: 48.1395%; height: 21px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 21px;&quot;&gt;윗첨자(지수)&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 21px;&quot;&gt;a^x&lt;/td&gt;
&lt;td style=&quot;width: 16.0466%; height: 21px;&quot;&gt;\(a^x\)&lt;/td&gt;
&lt;td style=&quot;width: 48.1395%; height: 21px;&quot;&gt;캐럿^ 뒤 문자가 윗첨자가 된다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 17px;&quot;&gt;아랫첨자(첨자)&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot;&gt;a_x&lt;/td&gt;
&lt;td style=&quot;width: 16.0466%; height: 17px;&quot;&gt;\(a_x\)&lt;/td&gt;
&lt;td style=&quot;width: 48.1395%; height: 17px;&quot;&gt;언더바_ 뒤의 문자가 아랫첨자가 된다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 17px;&quot;&gt;여러 문자&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot;&gt;a^{xy} 또는 a_{xy}&lt;/td&gt;
&lt;td style=&quot;width: 16.0466%; height: 17px;&quot;&gt;\( a^{xy} 또는 a_{xy} \)&lt;/td&gt;
&lt;td style=&quot;width: 48.1395%; height: 17px;&quot;&gt;두 글자 이상은 중괄호{}에 묶어야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 17px;&quot;&gt;제곱근&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot;&gt;\sqrt{x}&lt;/td&gt;
&lt;td style=&quot;width: 16.0466%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;\(\sqrt{x}\)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 48.1395%; height: 17px;&quot;&gt;\ 이건 반드시 붙여야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 17px;&quot;&gt;n제곱근&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot;&gt;\sqrt[n]{x}&lt;/td&gt;
&lt;td style=&quot;width: 16.0466%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;\(\sqrt[n]{x}\)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 48.1395%; height: 17px;&quot;&gt;대괄호[n]을 차수로 지정한다&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;b&gt;[참고 자료]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ysryuu.tistory.com/23&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ysryuu.tistory.com/23&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765334402036&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;[Blog] 티스토리에서 수식 추가하기&quot; data-og-description=&quot;머신러닝, 딥러닝 관련 포스팅을 위해서는 수식이 필요할 때가 종종 있을 것이다.티스토리에서 쉽게 수식을 추가하는 방법에 대해 포스팅해보겠다.&amp;nbsp;1. 티스토리 스킨 설정티스토리 설정 -&amp;gt; 스킨&quot; data-og-host=&quot;ysryuu.tistory.com&quot; data-og-source-url=&quot;https://ysryuu.tistory.com/23&quot; data-og-url=&quot;https://ysryuu.tistory.com/23&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bfTgqN/hyZO9isNV5/4OvIUvt4Lr655ruzloNY9K/img.png?width=800&amp;amp;height=240&amp;amp;face=0_0_800_240,https://scrap.kakaocdn.net/dn/ERRJX/hyZOOr4N1T/SG91C65AbA6U7c5aIIzjwk/img.png?width=800&amp;amp;height=240&amp;amp;face=0_0_800_240,https://scrap.kakaocdn.net/dn/Bmocf/hyZO7rpGDt/5tMajr2esSPH9KWeve5MKK/img.jpg?width=1889&amp;amp;height=1701&amp;amp;face=0_0_1889_1701&quot;&gt;&lt;a href=&quot;https://ysryuu.tistory.com/23&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ysryuu.tistory.com/23&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bfTgqN/hyZO9isNV5/4OvIUvt4Lr655ruzloNY9K/img.png?width=800&amp;amp;height=240&amp;amp;face=0_0_800_240,https://scrap.kakaocdn.net/dn/ERRJX/hyZOOr4N1T/SG91C65AbA6U7c5aIIzjwk/img.png?width=800&amp;amp;height=240&amp;amp;face=0_0_800_240,https://scrap.kakaocdn.net/dn/Bmocf/hyZO7rpGDt/5tMajr2esSPH9KWeve5MKK/img.jpg?width=1889&amp;amp;height=1701&amp;amp;face=0_0_1889_1701');&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;[Blog] 티스토리에서 수식 추가하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;머신러닝, 딥러닝 관련 포스팅을 위해서는 수식이 필요할 때가 종종 있을 것이다.티스토리에서 쉽게 수식을 추가하는 방법에 대해 포스팅해보겠다.&amp;nbsp;1. 티스토리 스킨 설정티스토리 설정 -&amp;gt; 스킨&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ysryuu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devjino.tistory.com/324&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devjino.tistory.com/324&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765334411710&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;[팁] tistory 수학 기호 넣기&quot; data-og-description=&quot;블러그 관리 -&amp;gt; 꾸미기 -&amp;gt; 스킨편집 -&amp;gt; html 편집 html 편집의 head부분에 다음의 코드를 넣어 줍니다. 사용 방법은 다음 페이지 참고합니다. https://ko.wikipedia.org/wiki/%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:TeX_%EB%AC&quot; data-og-host=&quot;devjino.tistory.com&quot; data-og-source-url=&quot;https://devjino.tistory.com/324&quot; data-og-url=&quot;https://devjino.tistory.com/324&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c6B6Qw/hyZOHGsa7E/AAk9KYEwwd7dDYQEfAlg90/img.png?width=295&amp;amp;height=256&amp;amp;face=0_0_295_256,https://scrap.kakaocdn.net/dn/b4d03A/hyZPgWdoyg/DiEfjZhh5dJbIsT2UxkCrk/img.png?width=295&amp;amp;height=256&amp;amp;face=0_0_295_256,https://scrap.kakaocdn.net/dn/vXgK3/hyZPf32pHT/bVp8H7pUtrdxoiMw24twkK/img.png?width=295&amp;amp;height=256&amp;amp;face=0_0_295_256&quot;&gt;&lt;a href=&quot;https://devjino.tistory.com/324&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devjino.tistory.com/324&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c6B6Qw/hyZOHGsa7E/AAk9KYEwwd7dDYQEfAlg90/img.png?width=295&amp;amp;height=256&amp;amp;face=0_0_295_256,https://scrap.kakaocdn.net/dn/b4d03A/hyZPgWdoyg/DiEfjZhh5dJbIsT2UxkCrk/img.png?width=295&amp;amp;height=256&amp;amp;face=0_0_295_256,https://scrap.kakaocdn.net/dn/vXgK3/hyZPf32pHT/bVp8H7pUtrdxoiMw24twkK/img.png?width=295&amp;amp;height=256&amp;amp;face=0_0_295_256');&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;[팁] tistory 수학 기호 넣기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;블러그 관리 -&amp;gt; 꾸미기 -&amp;gt; 스킨편집 -&amp;gt; html 편집 html 편집의 head부분에 다음의 코드를 넣어 줍니다. 사용 방법은 다음 페이지 참고합니다. https://ko.wikipedia.org/wiki/%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:TeX_%EB%AC&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devjino.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oWdHC/dJMcabpaZe2/aU99pOftYymn6Romk8fajk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oWdHC/dJMcabpaZe2/aU99pOftYymn6Romk8fajk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oWdHC/dJMcabpaZe2/aU99pOftYymn6Romk8fajk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoWdHC%2FdJMcabpaZe2%2FaU99pOftYymn6Romk8fajk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;300&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Etc &amp;zwj;♀️</category>
      <category>equation editor</category>
      <category>equationeditor</category>
      <category>Latex</category>
      <category>math</category>
      <category>mathematics</category>
      <category>기호</category>
      <category>수식</category>
      <category>수학</category>
      <category>수학기호</category>
      <category>티스토리</category>
      <author>JanuDev</author>
      <guid isPermaLink="true">https://janudev.tistory.com/51</guid>
      <comments>https://janudev.tistory.com/51#entry51comment</comments>
      <pubDate>Wed, 10 Dec 2025 17:04:50 +0900</pubDate>
    </item>
  </channel>
</rss>