<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>핀란드 교환학생의 개발일지</title>
    <link>https://programming-diary-ina.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 4 Jul 2026 19:27:45 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>iinana</managingEditor>
    <image>
      <title>핀란드 교환학생의 개발일지</title>
      <url>https://tistory1.daumcdn.net/tistory/6756207/attach/105bdb910cce433d814a6bef37bc6eb8</url>
      <link>https://programming-diary-ina.tistory.com</link>
    </image>
    <item>
      <title>[프로그래머스/Java] 입국심사(이분탐색 문제)</title>
      <link>https://programming-diary-ina.tistory.com/129</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;프로그래머스 알고리즘 고득점 Kit의 이분탐색 문제에 해당하는 '입국심사'를 자바로 풀어보았습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43238#&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/43238#&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1746061044367&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43238#&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bmx28P/hyYH9R85t2/TYfhpQk9FBXnVxuB2kVUD0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/ySqgC/hyYMRP1yS6/BCIJMpUqpI8mhKjyVDjjG0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43238#&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43238#&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bmx28P/hyYH9R85t2/TYfhpQk9FBXnVxuB2kVUD0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/ySqgC/hyYMRP1yS6/BCIJMpUqpI8mhKjyVDjjG0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;문제 접근&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;처음 이 문제를 접하고 들었던 생각은 어떻게 이 문제를 이분탐색으로 풀지?라는 것이었다. 내가 배워왔던 이분탐색과는 다른 느낌이었다. 하지만 '매개변수 탐색'이라는 개념을 접하고 나니 어떻게 이 문제를 풀어야 할지 알 수 있었다. 매개변수 탐색이란, 이분탐색을 이용해 최종답안에 가까워져 가는 탐색 방식을 말한다. 결국 '답이 될 결과'를 이분탐색하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;그래서 값이 될 수 있는 최솟값을 왼쪽 끝값으로, 최댓값을 오른쪽 끝값으로 두고, 이분탐색을 시행하면서 정답을 찾는 접근법으로 이 문제를 풀이해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;문제 풀이 코드&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1746061053273&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public long solution(int n, int[] times) {       
        // low, high, mid 설정
        Arrays.sort(times);
        long l = times[0], h = (long) times[times.length-1] * n;
        long m = (l + h) / 2;
        
        // 매개변수 이분탐색
        while (l &amp;lt; h) {
        	// 현재 탐색 중인 시간(m)에서 수용 가능한 n (avail_n) 계산
            long avail_n = 0;
            for (int t : times) {
                avail_n += (m / t);
            }
            
            // 이분탐색을 위한 h, l, m 재설정
            if (avail_n &amp;gt;= n) h = m;
            else l = m + 1;
            m = (l + h) / 2;
        }
        return m;
    }
}&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;시행착오 (주의할 점)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 문제에서 내가 겪었던 시행착오는 아래 두 가지이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. low와 high 설정 관련 문제&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;여기서 주의할 점은 총 두 가지이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&amp;nbsp;(1) low를 high와 비슷한 &quot;times[0] * n&quot;으로 설정해서는 안된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;low를 n번 곱하는 것보다 다른 심사대를 적절하게 섞어 쓰는 것이 더 적은 시간의 결과를 낼 수 있는 것은 당연하다. (만약 low에 n명이 모두 가는 것이 최솟값이었다면, 이 문제의 답은 그냥 low*n이 될 것이다.) 따라서 low값은 0이나 최소 소요 시간을 가진 심사대 값 자체로 설정해 두는 것이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(2) high 값을 구할 때 (long) 형 변환자를 꼭 써주어야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;만약 써주지 않는다면 high를 설정하면서 overflow가 발생하여 이상한 값이 high에 들어가 의도하지 않은 결과가 나올 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746062372521&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;long l = times[0], h = (long) times[times.length-1] * n;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. high, mid, low 재설정 관련 문제&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746062469438&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (avail_n &amp;gt;= n) h = m;
else l = m + 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 위 코드에서는 avail_n이 n 이상일 경우 h = m으로 설정한다. 이는 m이 유효한 답일 수 있으므로, m 값은 범위 내에 계속해서 포함하면서도 범위를 왼쪽으로 좁히기 위한 코드이다. 그런데 처음에는 아래 코드도 비슷하게 성립할 수 있다고 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746063752565&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (avail_n &amp;gt; n) h = m - 1;
else l = m;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;549&quot; data-start=&quot;436&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;겉보기에는 비슷한 조건처럼 보이지만, 이 두 조건은 이진 탐색의 수렴 방식에서 중요한 차이를 만든다. 실제로 두 번째 방식은 일부 입력에서 무한 루프에 빠지는 문제가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;749&quot; data-start=&quot;551&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그 이유는 m의 계산 방식 때문이다. m = (l + h) / 2는 정수 나눗셈이기 때문에 소수점 이하는 버려지며, 항상 l 쪽으로 치우친 값(floor 값)이 된다. 따라서 l = m을 하면 l이 변하지 않고 루프를 무한으로 돌면서도 계속 같은 값으로 초기화되는 상황이 발생할 수 있다. 예를 들면 아래와 같은 상황이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;l&amp;nbsp;=&amp;nbsp;3,&amp;nbsp;h&amp;nbsp;=&amp;nbsp;4 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;m&amp;nbsp;=&amp;nbsp;(3&amp;nbsp;+&amp;nbsp;4)&amp;nbsp;/&amp;nbsp;2&amp;nbsp;=&amp;nbsp;3 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;l = m &amp;rarr; l은 여전히 3 + h는 4 &amp;rarr; 다음에도 m = 3 &amp;rarr; 무한 반복 &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;비슷한 논리로 만약 Math.ceil()을 사용해서 m이 올림값이 되도록 한다면 반대로 두 번째 코드를 사용해야 무한 루프를 방지할 수 있을 것이다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>기초 공부 (언어 및 알고리즘)/알고리즘 (Java)</category>
      <author>iinana</author>
      <guid isPermaLink="true">https://programming-diary-ina.tistory.com/129</guid>
      <comments>https://programming-diary-ina.tistory.com/129#entry129comment</comments>
      <pubDate>Thu, 1 May 2025 10:51:19 +0900</pubDate>
    </item>
    <item>
      <title>[python / colab] 증권사 리포트 pdf 파일 크롤링하기</title>
      <link>https://programming-diary-ina.tistory.com/128</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;대표적으로 증권사 리포트를 확인할 수 있는 사이트인 한경컨센서스와 네이버금융리서치를 크롤링하여 리포트 pdf를 구글 드라이브에 저장하는 작업을 코랩에서 수행해 봤다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ffffff; background-color: #1a5490; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&amp;nbsp;공통 단계&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;사전 준비&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1745449751757&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 구글 드라이브 마운트 (구글 드라이브와 코드 연결)
from google.colab import drive
drive.mount('/content/drive')&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745449761980&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# selenium 설치
!pip install selenium
!apt-get update&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745449789342&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 구글 드라이브에 choremdriver 설치 (최초 1회)
!apt install chromium-chromedriver
!cp /usr/bin/chromedriver &quot;/&quot; #chorme driver 설치할 dir
!pip install chromedriver-autoinstaller&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745449823152&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 그 외 필요한 패키지들 다운
!pip install requests
!pip install PyPDF2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;실행 준비&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1745449860480&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 라이브러리 임포트
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import sys
from selenium.webdriver.common.keys import Keys
import urllib.request
import os
from urllib.request import urlretrieve

import time
import pandas as pd
import chromedriver_autoinstaller  # setup chrome options&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745449869974&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# chrome_options 설정 (크롬드라이버 실행 옵션)
## chromedriver 경로 명시
chrome_path = &quot;&quot; # chrome driver dir
sys.path.insert(0,chrome_path)

chrome_options = webdriver.ChromeOptions()
## colab은 새 창을 지원하지 않기 때문에 --headless 옵션 필요 (브라우저 창을 띄우지 않고 백그라운드에서 실행)
chrome_options.add_argument('--headless')
## 보안 샌드박스 없이 실행 (제한된 환경인 colab에서 필요한 설정)
chrome_options.add_argument('--no-sandbox')
## colab의 용량 제한으로 인한 오류 방지
chrome_options.add_argument('--disable-dev-shm-usage')
## chrome 언어 한국어로 설정
chrome_options.add_argument('lang=ko_KR')

## set the target URL
chromedriver_autoinstaller.install()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp; 위 옵션 설정에서 중요한 부분은 argument 중 하나인 --headless는 colab이 새 창을 지원하지 않기 때문에 필요한 옵션으로, 만약 로컬 환경을 사용한다면 추가하지 않아도 된다. 이러한 특성 때문에 실제로 크롤링이 돌아가는 창을 확인하며 내가 의도한 대로 실행되는지 확인할 수 없다는 단점이 있다. 나는 그래서 디버깅을 할 때 아래 코드를 이용해 문제가 된다고 예상되는 파트에서 백그라운드로 실행 중인 화면의 스크린숏을 남겨 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745452051530&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;driver.save_screenshot(&quot;screen.png&quot;)
from IPython.display import Image
Image(&quot;screen.png&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #1a5490; color: #ffffff; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&amp;nbsp;크롤링 - 한경 컨센서스&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://markets.hankyung.com/consensus&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://markets.hankyung.com/consensus&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1745452109950&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;한경 컨센서스 | 한국경제&quot; data-og-description=&quot;주간 목표 상향 TOP10 리포트 조회 검색결과가 없습니다. 코리아마켓 금융 정보는 각 콘텐츠 제공업체로부터 받는 투자 참고사항이며, 오류가 발생하거나 지연될 수 있습니다. 한경닷컴과 콘텐츠&quot; data-og-host=&quot;markets.hankyung.com&quot; data-og-source-url=&quot;https://markets.hankyung.com/consensus&quot; data-og-url=&quot;https://markets.hankyung.com/consensus&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://markets.hankyung.com/consensus&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://markets.hankyung.com/consensus&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;한경 컨센서스 | 한국경제&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;주간 목표 상향 TOP10 리포트 조회 검색결과가 없습니다. 코리아마켓 금융 정보는 각 콘텐츠 제공업체로부터 받는 투자 참고사항이며, 오류가 발생하거나 지연될 수 있습니다. 한경닷컴과 콘텐츠&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;markets.hankyung.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;pre id=&quot;code_1745449888310&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# url과 driver set
main_url = &quot;https://markets.hankyung.com/consensus&quot;
login_url = &quot;https://id.hankyung.com/login/login.do&quot;
driver =  webdriver.Chrome(options=chrome_options)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; &amp;nbsp;한경 컨센서스의 경우 로그인을 해야 리포트 파일에 접근 가능하기 때문에 본격적인 크롤링 전 꼭 로그인 단계를 원격으로 실행해줘야 한다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745449897040&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 한경 컨센서스 로그인
driver.get(login_url)
user_id = driver.find_element(By.ID, 'userId')
user_id.send_keys(&quot;&quot;) #한경컨센서스 아이디 입력

user_pw = driver.find_element(By.ID, 'userPass')
user_pw.send_keys(&quot;&quot;) #한경컨센서스 비밀번호 입력
user_pw.send_keys(Keys.RETURN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;리포트를 순회하면서 가져오는 방식은 현재 페이지에서 가장 첫 번째 리포트의 id를 가져오고 맨 끝 페이지로 이동해서 맨 마지막 리포트의 id를 가져와서 순회하는 식이다. 가장 첫 리포트부터 마지막 리포트까지 하나씩 감소하면서 id가 부여되는 방식이며 리포트 pdf의 url이 base_url + id의 형식을 지켰기에 가능한 방식이었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;또한, 마지막 리포트에 접근할 때는 검색 탭에서 기간을 설정하여 접근 가능하도록 코드를 작성했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745449905412&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# report 시작 id 가져오기
import re

driver.get(main_url)
time.sleep(3)

a_tags = driver.find_elements(By.CSS_SELECTOR, 'a.ellip')
href = a_tags[0].get_attribute('href')
start_id = re.search(r'/consensus/view/(\d+)', href).group(1)
start_id = int(start_id)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745449916082&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# report 끝 id 가져오기
from selenium.webdriver.support.ui import Select

driver.get(main_url)
time.sleep(3)

## 기간 선택 (시작 id에서는 불필요)
dropdown_arrow = driver.find_element(By.CLASS_NAME, 'select2-selection__arrow')
dropdown_arrow.click()
time.sleep(1)

# &quot;최근 7일&quot;, &quot;1개월&quot;, &quot;6개월&quot;, &quot;1년&quot;
recent_7 = driver.find_element(By.XPATH, '//li[contains(text(), &quot;최근 7일&quot;)]')
recent_7.click()
time.sleep(1)

search_button = driver.find_element(By.CLASS_NAME, &quot;btn-primary&quot;)
search_button.click() # 검색 버튼 클릭
time.sleep(1)

## 끝 페이지로 이동
end_page=driver.find_element(By.CLASS_NAME, 'btn-page-end')
end_page.click()

a_tags = driver.find_elements(By.CSS_SELECTOR, 'a.ellip')
href = a_tags[-1].get_attribute('href')
end_id = re.search(r'/consensus/view/(\d+)', href).group(1)
end_id = int(end_id)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;마지막으로 마지막 리포트의 id ~ 첫 리포트의 id를 순회하면서 pdf를 지정된 구글 드라이브 directory에 저장하며 끝낸다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745449934070&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import requests
from io import BytesIO
from PyPDF2 import PdfReader

base_url = &quot;https://markets.hankyung.com/consensus/view/&quot;
# Selenium 세션에서 쿠키 가져오기
cookies = driver.get_cookies()

# requests 세션 생성
session = requests.Session()
for cookie in cookies:
    session.cookies.set(cookie['name'], cookie['value'])

for i in range(end_id, start_id):
  driver.get(base_url + str(i))
  time.sleep(3)

  title=driver.find_element(By.CLASS_NAME, &quot;report-tit&quot;)
  title=title.text.strip()
  title=re.sub(r'[\\/*?:&quot;&amp;lt;&amp;gt;|]', &quot;&quot;, title)

  file_url=driver.find_element(By.CSS_SELECTOR, 'a.btn.btn-default').get_attribute('href')
  driver.get(file_url)
  time.sleep(3)

  response = session.get(file_url)
  if response.status_code == 200:
      with open(&quot;#pdf 저장할 dir&quot;+title+&quot;.pdf&quot;, &quot;wb&quot;) as f:
          f.write(response.content)
      print(&quot;PDF 다운로드 완료.&quot;)
  else:
      print(&quot;PDF 파일 다운로드 실패.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #1a5490; color: #ffffff;&quot;&gt;&amp;nbsp;크롤링 - 네이버 증권 리서치&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://finance.naver.com/research/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://finance.naver.com/research/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1745452387816&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;리서치 : 네이버페이 증권&quot; data-og-description=&quot;관심종목의 실시간 주가를 가장 빠르게 확인하는 곳&quot; data-og-host=&quot;finance.naver.com&quot; data-og-source-url=&quot;https://finance.naver.com/research/&quot; data-og-url=&quot;https://finance.naver.com/research/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/f5A3t/hyYJwMUtrU/0h7pdpuKVsJ1ojxPHmm9x1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://finance.naver.com/research/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://finance.naver.com/research/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/f5A3t/hyYJwMUtrU/0h7pdpuKVsJ1ojxPHmm9x1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;리서치 : 네이버페이 증권&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;관심종목의 실시간 주가를 가장 빠르게 확인하는 곳&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;finance.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;네이버 증권 리서치의 장점은 로그인 없이 리포트 pdf 파일에 접근할 수 있다는 점이다. 한경 컨센서스처럼 별도로 원격 로그인 하는 코드를 쓸 필요가 없다. 다만, 종목/산업/시황/투자정보 리포트가 약간 다른 형태의 html로 주어져서 관련해서 살짝만 수정해 주면 된다. 또, 종목/산업 리포트끼리는 형식이 같고, 시황/투자정보 리포트끼리도 형식이 같다. 나는 종목/산업 분석 리포트를 중심으로 작성했다. 사실 시황/투자정보라고 하더라도 인덱스만 확인하고 살짝 수정해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745457031987&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# url과 driver set
url = &quot;https://finance.naver.com/research/company_list.naver?&amp;amp;page=&quot; #종목분석 리포트
#url = &quot;https://finance.naver.com/research/industry_list.naver?page=&quot; #산업분석 리포트

save_dir = &quot;/&quot; # pdf 저장할 directory
driver =  webdriver.Chrome(options=chrome_options)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;앞선 한경 컨센서스 크롤링에서는 검색탭에 주어지는 기간 설정을 이용했지만, 네이버 금융 리서치의 경우 그냥 직접 날짜를 확인하는 편이 더 간편해 보여서 그렇게 진행했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745457183589&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 크롤링 기간 설정
from datetime import datetime, timedelta

end_date = datetime.now() # 오늘 날짜
start_date = end_date - timedelta(days=7) # n일 전 날짜&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745457204670&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import requests
from io import BytesIO
from PyPDF2 import PdfReader

# Selenium 세션에서 쿠키 가져오기
cookies = driver.get_cookies()

# requests 세션 생성
session = requests.Session()
for cookie in cookies:
    session.cookies.set(cookie['name'], cookie['value'])

page = 1
while True:
  try:
    driver.get(url + str(page))
    time.sleep(3)
  except Exception:
    continue

  reports_tab = driver.find_element(By.XPATH, &quot;/html/body/div[3]/div[2]/div[2]/div[1]/div[2]/table[1]/tbody&quot;)
  reports = reports_tab.find_elements(By.TAG_NAME, &quot;tr&quot;) # 리포트 목록
  
  for r in reports:
    tds = r.find_elements(By.TAG_NAME, &quot;td&quot;)
    if len(tds) &amp;lt; 4:
       continue

    company = tds[0].text.strip() # 회사 이름
    date = tds[4].text.strip() # 작성일

    ## 기간 확인
    try:
      report_date = datetime.strptime(date, &quot;%y.%m.%d&quot;)
    except ValueError:
            continue
    if not (start_date &amp;lt;= report_date &amp;lt;= end_date):
        continue  # 원하는 날짜 범위가 아니면 스킵

    ## pdf 파일 url 추출
    file = tds[3].find_element(By.TAG_NAME, &quot;a&quot;).get_attribute(&quot;href&quot;)
    driver.get(file)
    time.sleep(3)
    
    ## pdf 파일 저장
    response = session.get(file)
    time.sleep(3)
    if response.status_code == 200:
        with open(save_dir+company+&quot;_&quot;+date+&quot;.pdf&quot;, &quot;wb&quot;) as f:
            f.write(response.content)
        print(&quot;PDF 다운로드 완료.&quot;)
    else:
        print(&quot;PDF 파일 다운로드 실패.&quot;)

  page += 1&lt;/code&gt;&lt;/pre&gt;</description>
      <category>학교 공부/졸업 프로젝트 (RAG)</category>
      <author>iinana</author>
      <guid isPermaLink="true">https://programming-diary-ina.tistory.com/128</guid>
      <comments>https://programming-diary-ina.tistory.com/128#entry128comment</comments>
      <pubDate>Thu, 24 Apr 2025 10:14:58 +0900</pubDate>
    </item>
    <item>
      <title>[SpringBoot/JWT] 로그인/회원가입에 Refresh Token 도입하기</title>
      <link>https://programming-diary-ina.tistory.com/127</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;0. JWT 기반 로그인/회원가입 구현&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;기존에 구현되어 있던 JWT 기반 로그인/회원가입에서 보안과 안정성 향상을 위해 refresh token을 추가해 줬다. 이미 구현된 JWT 기반 로그인/회원가입과 왜 refresh token이 필요한지에 대한 내용은 아래 글에서 확인할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://programming-diary-ina.tistory.com/126&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://programming-diary-ina.tistory.com/126&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1744617968767&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/JWT] JWT로 로그인/로그아웃 구현하기&quot; data-og-description=&quot;토큰 기반 인증&amp;nbsp;1. 토큰 기반 인증이란&amp;nbsp;토큰을 사용하여 인증하는 방식이다. 서버가 토큰을 생성해서 클라이언트에게 제공하면, 클라이언트는 이 토큰을 가지고 있다가 여러 요청을 토큰과 함&quot; data-og-host=&quot;programming-diary-ina.tistory.com&quot; data-og-source-url=&quot;https://programming-diary-ina.tistory.com/126&quot; data-og-url=&quot;https://programming-diary-ina.tistory.com/126&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vLH8G/hyYEELcCDV/m9U7rwH1CiRs84kfTvra41/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/IDx6t/hyYEGhYII9/7KUuekaTNR4KK1vuKpoqP0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://programming-diary-ina.tistory.com/126&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://programming-diary-ina.tistory.com/126&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vLH8G/hyYEELcCDV/m9U7rwH1CiRs84kfTvra41/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/IDx6t/hyYEGhYII9/7KUuekaTNR4KK1vuKpoqP0/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;[Spring Boot/JWT] JWT로 로그인/로그아웃 구현하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;토큰 기반 인증&amp;nbsp;1. 토큰 기반 인증이란&amp;nbsp;토큰을 사용하여 인증하는 방식이다. 서버가 토큰을 생성해서 클라이언트에게 제공하면, 클라이언트는 이 토큰을 가지고 있다가 여러 요청을 토큰과 함&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programming-diary-ina.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;refresh token의 추가로, 기존 코드에서는 access를 위한 한 종류의 token만 존재했지만, 이제는 refresh token과 access token, 두 가지 종류의 토큰이 존재하게 된다. refresh token은 쉽게 말하면 access token 재발급을 위한 token이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. RefreshToken entity 및 repository 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;refresh token을 생성 및 저장하기 위해 entity와 repository를 만들어준다. entity 코드는 아래와 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744618337274&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** domain/RefreshToken.java **/
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class RefreshToken {
    // refresh token 자체의 id
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 사용자 id
    @Column(name = &quot;user_id&quot;, nullable = false, unique = true)
    private Long userId;

    // refresh token 값
    @Column(name = &quot;refresh_token&quot;, nullable = false)
    private String refreshToken;

    public RefreshToken(Long userId, String refreshToken) {
        this.userId = userId;
        this.refreshToken = refreshToken;
    }

    public RefreshToken update(String newRefreshToken) {
        this.refreshToken = newRefreshToken;
        return this;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;다음은 생성된 refresh token entity를 저장할 repository이다. JPA 기반으로 생성해준다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744618450832&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** repository/RefreshTokenRepository.java **/
import com.survey.server.domain.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface RefreshTokenRepository extends JpaRepository&amp;lt;RefreshToken, Long&amp;gt; {
    Optional&amp;lt;RefreshToken&amp;gt; findByUserId(Long userId);
    Optional&amp;lt;RefreshToken&amp;gt; findByRefreshToken(String refreshToken);

    void deleteByUserId(Long userId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. token service와 refresh token service&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;다음으로 앞서 설계해둔 refresh token 로직을 실질적으로 사용하기 위해 refresh token service와 token serivce를 만들어준다. refresh token service는 refresh token을 생성하고 삭제하기 위한 service이고, token service는 refresh token을 기반으로 access token을 생성하기 위한 service이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;/** service/RefreshTokenService.java **/
import com.survey.server.config.jwt.TokenProvider;
import com.survey.server.domain.RefreshToken;
import com.survey.server.repository.RefreshTokenRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.time.Duration;

@RequiredArgsConstructor
@Service
public class RefreshTokenService {
    private final TokenProvider tokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;
    private static final Long REFRESH_EXPIRATION = Duration.ofDays(14).toMillis();

    // refresh token 찾기
    public RefreshToken findByRefreshToken(String refreshToken) {
        return refreshTokenRepository.findByRefreshToken(refreshToken)
                .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;Unexpected Refresh Token&quot;));
    }
    
    // 새로운 refresh token 생성
    public String createNewRefreshToken(Long userId) {
        // refresh token 값 생성
        String refreshToken = tokenProvider.makeToken(userId, null, REFRESH_EXPIRATION);
        // roles = null로 설정 (refresh token은 null 값 필요하지 않음)

        // refresh token entity 생성 및 저장
        RefreshToken refreshTokenEntity = new RefreshToken(userId, refreshToken);
        refreshTokenRepository.save(refreshTokenEntity);

        return refreshToken;
    }

    // 이미 사용한 refresh token 업데이트
    public RefreshToken updateRefreshToken(Long userId, String refreshToken) {
        RefreshToken refreshTokenEntity = findByRefreshToken(refreshToken);
        if (refreshTokenEntity == null) {
            System.err.println(&quot;Refresh Token not found&quot;);
            throw new IllegalArgumentException(&quot;Unexpected Refresh Token&quot;);
        }
        refreshTokenEntity.update(tokenProvider.makeToken(userId, null, REFRESH_EXPIRATION));
        return refreshTokenRepository.save(refreshTokenEntity);
    }

    // 로그아웃 시 refresh token 삭제
    @Transactional
    public void delete() {
        // 현재 로그인 되어있는 유저 찾기
        String token = SecurityContextHolder.getContext().getAuthentication().getCredentials().toString();
        Long userId = tokenProvider.getUserId(token);
        
        // 해당 유저의 refresh token 삭제
        refreshTokenRepository.deleteByUserId(userId);
    }

    // refresh token 만료 시 DB에서 삭제
    public void expire(String refreshToken) {
        RefreshToken refreshTokenEntity = findByRefreshToken(refreshToken);
        if (refreshTokenEntity != null) {
            refreshTokenRepository.deleteByUserId(refreshTokenEntity.getUserId());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;다음은 token service이다. 여기서는 refresh token이 유효한지 검증하고, 유효하다면 access token을 새로 발급하면서, 이미 사용된 refresh token도 폐기하고 다시 생성한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744618815210&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** service/TokenService.java **/
import com.survey.server.config.jwt.TokenProvider;
import com.survey.server.domain.RefreshToken;
import com.survey.server.model.User;
import com.survey.server.repository.RefreshTokenRepository;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Set;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
public class TokenService {
    private final TokenProvider tokenProvider;
    private final RefreshTokenService refreshTokenService;

    // 새로운 access token 생성
    public String createNewAccessToken(String refreshToken, Set&amp;lt;String&amp;gt; roles) {
        // refresh token 유효성 검증
        if (!tokenProvider.isValidToken(refreshToken)) {
            refreshTokenService.expire(refreshToken); // 만료된 refresh token 삭제
            throw new IllegalArgumentException(&quot;Invalid refresh token&quot;);
        }

        Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
        
        // refresh token update
        refreshTokenService.deleteByUserId(userId);
        refreshTokenService.createNewRefreshToken(userId);

        // access token 재발급
        return tokenProvider.makeToken(userId, roles);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 로그인에 refresh token 생성 로직 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;로그인 시에 바로 access token을 생성하던 것에서 refresh token을 생성한 후 이를 기반으로 access token을 생성하는 것으로 로직을 수정한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744619219041&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** controller/UserController.java **/
@PostMapping(&quot;/login&quot;)
public ResponseEntity&amp;lt;LoginResponseDTO&amp;gt; login(@RequestBody @Validated LoginRequestDTO request, HttpServletResponse response) {
    try {
        LoginResponseDTO result = userService.login(request, response);
        return ResponseEntity.ok(result);
    } catch (IllegalStateException e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1744619261057&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** service/UserService.java **/
public LoginResponseDTO login(LoginRequestDTO request, HttpServletResponse response) {
    Optional&amp;lt;User&amp;gt; optionalUser = userRepository.findByUserIdent(request.getUserIdent());
    if (optionalUser.isEmpty() ||
            !bCryptPasswordEncoder.matches(request.getUserPassword(), optionalUser.get().getPasswordHash())) {
        throw new IllegalStateException(&quot;로그인 실패: invalid_credentials&quot;);
    }

    // refresh token 생성
    User user = optionalUser.get();
    String refreshToken = refreshTokenService.createNewRefreshToken(user.getUserId());
    CookieUtil.addCookie(response, &quot;refresh_token&quot;, refreshToken, 7 * 24 * 60 * 60); //7일

    // access token 생성
    Set&amp;lt;String&amp;gt; roles = user.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toSet());
    String accessToken = tokenService.createNewAccessToken(refreshToken, roles);

    return new LoginResponseDTO(
            accessToken,
            user.getUserIdent(),
            roles
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;6. TokenController 작성으로 클라이언트로부터 access token 발급 요청받기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;마지막으로 access token이 만료되었을 때, 클라이언트가 refresh token을 가지고 재발급받을 수 있도록 TokenController를 작성한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745135124339&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** controller/TokenController.java **/
import com.survey.server.dto.CreateAccessTokenRequest;
import com.survey.server.dto.CreateAccessTokenResponse;
import com.survey.server.service.RefreshTokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import com.survey.server.service.TokenService;

import java.util.Set;

@Controller
@RequestMapping(&quot;/api&quot;)
@RequiredArgsConstructor
public class TokenController {
    private final TokenService tokenService;
    private final RefreshTokenService refreshTokenService;

    /**
     * access token 발급 endpoint
     * @param request access token 발급 요청
     * @return token 발급 응답 DTO를 담은 Response Entity
     */
    @PostMapping(&quot;/token&quot;)
    public ResponseEntity&amp;lt;CreateAccessTokenResponse&amp;gt; createNewAccessToken(@RequestBody CreateAccessTokenRequest request) {
        try {
            String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken(), Set.of(&quot;ROLE_USER&quot;));
            return ResponseEntity.status(HttpStatus.CREATED)
                    .body(new CreateAccessTokenResponse(newAccessToken));
        } catch (IllegalArgumentException e) {
            // 유효하지 않은 refresh token
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발 프로젝트/빅데이터마케팅랩 Server</category>
      <author>iinana</author>
      <guid isPermaLink="true">https://programming-diary-ina.tistory.com/127</guid>
      <comments>https://programming-diary-ina.tistory.com/127#entry127comment</comments>
      <pubDate>Sun, 20 Apr 2025 16:46:11 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot/JWT] JWT로 로그인/로그아웃 구현하기</title>
      <link>https://programming-diary-ina.tistory.com/126</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #1a5490; color: #ffffff; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&amp;nbsp;토큰 기반 인증&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 토큰 기반 인증이란&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;토큰을 사용하여 인증하는 방식이다. 서버가 토큰을 생성해서 클라이언트에게 제공하면, 클라이언트는 이 토큰을 가지고 있다가 여러 요청을 토큰과 함께 신청한다. 서버는 토큰을 보고 유효한 사용자인지 검증하여 요청을 수행해준다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 세션 기반 인증과 토큰 기반 인증&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;세션 기반 인증은 사용자마다 사용자의 정보를 담은 세션을 생성하고 저장해서 인증하는 방식이다. 세션 기반 인증과 토큰 기반 인증의 가장 큰 차이는 요청과 함께 전달하는 정보의 양 차이이다. 토큰 기반 인증의 경우, 검증에 필요한 정보를 모두 담은 토큰을 요청과 함께 전달한다. 즉, 토큰에 회원정보와 유효기간 같은 검증에 필요한 정보도 담겨있어, 토큰만 확인하면 회원의 유효성을 입증할 수 있다. 하지만 세션 기반 인증의 경우, 토큰과 달리 최소한의 정보만을 담아서 요청과 함께 서버에 보낸다. 그러면 서버에서는 메모리나 DB에 저장된 정보에서 전달받은 정보를 기반으로 유효한 사용자인지 검증을 하는 방식이다. 하지만 토큰 기반 인증의 경우, 토큰만 확인하면 되기 때문에 따로 정보를 저장해둘 필요도, 이를 검색하여 유효성을 검증할 필요도 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. 토큰 기반 인증의 특징&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;위에서 세션 기반 인증과의 차이점으로 인해 발현되는 토큰 기반 인증의 특징들이 있다. 특징이자 jwt의 장점이기도 하다. 그 특징들은 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;무상태성: 서버에 별도로 인증 정보를 저장할 필요 없음 (클라이언트가 토큰을 가지고 있음)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;확장성: 서버 확장 시 상태 관리에 신경 쓸 필요가 없음 (하나의 토큰으로 다양한 서버에 요청을 보낼 수 있음)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;무결성: 토큰을 발급한 이후 누군가 토큰 정보를 변경하는 행위를 할 수 없음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #1a5490; color: #ffffff; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&amp;nbsp;JWT&amp;nbsp; (Json Web Token)&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. JWT의 토큰 구성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;발급받은 JWT를 이용해 인증을 하려면 HTTP 요청 헤더 중 Authorization 키 값에 &quot;Bearer &quot; + JWT 토큰값을 넣어 보내야 한다. JWT 토큰값은 헤더(header), 내용(payload), 서명(signature)로 구성된다. 이 구성 정보들을 BASE64로 인코딩한 값이 JWT 토큰값이 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(1) 헤더(header): 토큰의 타입과 해싱 알고리즘을 지정하는 정보&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742648290560&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;typ&quot;: &quot;JWT&quot;,
    &quot;alg&quot;: &quot;HS256&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(2) 내용(payload): 토큰과 관련된 정보를 담음. 내용 한 덩어리를 클레임(claim)이라고 하며, 공개되어도 상관없는 클레임인 공개 클레임, 공개되면 안되는 클레임인 비공개 클레임, 마지막으로 JWT 자체에서 등록된 등록된 클레임이 있다. 등록된 클레임은 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;iss : issuer (토큰 발급자)&lt;/span&gt;&lt;/li&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;sub : subject (토큰 제목)&lt;/span&gt;&lt;/li&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;aud: audience (토큰 대상자)&lt;/span&gt;&lt;/li&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;exp: experation (토큰 만료 시간, NumericDate 형식)&lt;/span&gt;&lt;/li&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;nbf: not before (토큰의 활성 날짜, NumericDate 형식)&lt;/span&gt;&lt;/li&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;iat: issued at (토큰 발급 시간)&lt;/span&gt;&lt;/li&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;jti: JWT의 고유 식별자 (일회용 토큰에서 주로 사용)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(3) 서명(signiture): 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도로 사용된다. header와 payload의 인코딩 값을 합친 후 secret key를 넣는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. refresh token&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;토큰의 유효기간이 길면 토큰 탈취의 위험이 있고, 유효기간이 짧으면 재발급의 불편이 있으므로, 이를 해결하기 위해 refresh token이 사용된다. 액세스 토큰의 유효기간을 짧게 설정하고, refresh token의 유효기간은 길게 설정하면, 엑세스 토큰이 탈취당해도 짧은 유효기간 이후에 사용할 수 없으므로 보다 안전해진다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;작동하는 방식은 액세스 토큰으로 요청을 전달했는데, 만료된 액세스 토큰인 경우 refresh token과 함께 액세스 토큰 발급 요청을 해서 새로운 액세스 토큰을 받는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. JWT로 인증 구현 시 주의할 점&lt;/b&gt; (&lt;a href=&quot;https://www.youtube.com/watch?v=XXseiON9CV0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=XXseiON9CV0&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(1) 헤더 부분의 알고리즘을 none으로 채우지 말 것&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;알고리즘을 none으로 만들면, 이를 이용해서 공격하는 것으로부터 안전할 수 없다. 따라서, none으로 만든 JWT 입장권들을 거절하는 기능이 있는지 확인해야 한다. 최신 라이브러리(HS256 등)를 잘 사용하면 안전하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742649032702&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;alg&quot;: &quot;none&quot;, //위험 -&amp;gt; &quot;HS256&quot; 사용
    &quot;typ&quot;: &quot;JWT&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(2) 최소한의 정보만 넣기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;JWT는 변환이 쉽기 때문에, 민감한 정보들은 빼고, 인증을 위한 최소한의 정보만 넣는 것이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(3) 시크릿키를 제대로 작성&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;너무 짧거나 추측하기 쉬운 secret key로 설정을 하게 되면, 예측이 쉬워져서 bruteforce attack을 받을 수 있다. secret key가 유출되면 token이 외부에 의해 마음대로 발행되는 일이 발생할 수 있다. 따라서 키를 매우 길게 설정하고, 공유하지 않는 것이 중요하다. 조금 더 엄격하게 관리하고 싶다면, 생성용키와 검증용키를 따로 생성하여 사용하는 방법도 있다.&amp;nbsp;&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;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ffffff; background-color: #1a5490;&quot;&gt;&amp;nbsp;JWT로 로그인/로그아웃 구현하기&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;├── src &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ ├── main &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ ├── java&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp;└── com.study.blog_project&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;├── config&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├── jwt&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;├── JwtProperties.java&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // 토큰제공자&amp;amp;secretKey 변수로 가져오기&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;├── TokenAuthenticationFilter.java&amp;nbsp; &amp;nbsp; &amp;nbsp; // 토큰 필터&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;└── TokenProvider.java&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // 토큰생성&amp;amp;유효성검증&amp;amp;인증정보가져오기&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├── RedisConfig.java&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // 인증코드 저장을 위한 Redis 저장소 설정&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; └── WebSecurityConfig.java&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 접근권한, password encoder 설정&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;├── controller&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; └── UserController.java&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; │ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;├── dto&amp;nbsp; &amp;nbsp;// 편의상 알파벳 순이 아닌 서비스 로직 순으로 작성&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├──&amp;nbsp; SignupRequestDTO.java&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // 회원가입&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├──&amp;nbsp; SignupResponseDTO.java&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├──&amp;nbsp; IdentConfirmDTO.java&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 아이디 중복확인 (회원가입 시)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├──&amp;nbsp; LoginRequestDTO.java&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // 로그인&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├──&amp;nbsp; LoginResponseDTO.java&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├──&amp;nbsp; FindIdentRequestDTO.java&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 아이디, 비밀번호 찾기&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├──&amp;nbsp; FindIdentResponseDTO.java&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├──&amp;nbsp; FindPasswordRequestDTO.java&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├── PasswordUpdateDTO.java&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 비밀번호 변경 (비밀번호 찾기 시)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├── VerificationEmailSendDTO.java&amp;nbsp; &amp;nbsp; &amp;nbsp; // 인증메일 전송&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; └── CodeVeriyDTO.java&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // 인증번호 확인&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;├── model&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; └── User.java&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;├── repository&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├── UserRepository.jva&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; └── VerificationRepository.java&amp;nbsp; &amp;nbsp; &amp;nbsp; // 인증번호 저장용 Redis Repository&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;└── service&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #666666;&quot;&gt; │ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├── MailService.java&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #666666;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; └── UserService.java&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ └── resources&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp;├── application.yml&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;│ │ │&amp;nbsp; &amp;nbsp; &amp;nbsp;└── application-secret.yml&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;│ &lt;/span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;└── build.gradle&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;0. 기본 준비&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(1) 의존성 추가하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742796798219&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** build.gradle **/
dependencies {
    // spring web
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // validation
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // java mail sender
    implementation 'org.springframework.boot:spring-boot-starter-mail'

    // jwt
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    implementation 'javax.xml.bind:jaxb-api:2.3.1'

    // lombok
    implementation 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // spring security
    implementation 'org.springframework.boot:spring-boot-starter-security'
    testImplementation 'org.springframework.security:spring-security-test'

    // DB &amp;amp; caching
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'  // jpa
    implementation 'org.mariadb.jdbc:mariadb-java-client:3.1.2' // maria db
    implementation 'org.springframework.boot:spring-boot-starter-data-redis' // redis

    // gson for redis config
    implementation 'com.google.code.gson:gson:2.8.6'

    // test
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(2) application.yml 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;민감한 정보는 application-secret.yml에 따로 빼서 넣었다. 어떤 정보가 있는지는 주석으로 표기되어 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(관련 글: &lt;a href=&quot;https://programming-diary-ina.tistory.com/116&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://programming-diary-ina.tistory.com/116 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;)&lt;/span&gt; &lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742791177590&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;## application.yml ##
spring:
  application:
    name: survey-server

  # DB 연결
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://127.0.0.1:3306/survey?serverTimezone=Asia/Seoul

  # 인증 메일 전송
  mail:
    host: smtp.gmail.com
    port: 587
    properties:
      mail.smtp.auth: true
      mail.smtp.starttls.enable: true

  # jwt token
  jwt:
    expiration: 3600000 #1시간(단위:ms)

  config:
    import: application-secret.yml
    #datasource:
    #  username: username
    #  password: password
    #jwt:
    #  issuer: issuer_email
    #  secret_key: secret_key
    #  expiration: 3600000
    #mail:
    #  username: email_address
    #  password: email_password

  # db 연결용 jpa
  jpa:
    properties:
      hibernate:
        show_sql: true
        format_sql: true
    hibernate:
      ddl-auto: create
    database-platform: org.hibernate.dialect.MySQLDialect

# 로깅
logging:
  level:
    root: DEBUG
    com.survey: DEBUG&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. JWT Token 관련 로직&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(1) application.yml에서 설정한 jwt properties 불러오기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;application.yml 파일에 설정해 둔 token issuer, secretKey, 그리고 토큰 만료 기간에 해당하는 expiration을 주입 받아 사용할 수 있도록 JwtProperties 파일을 만들어서 불러온다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742791498687&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** JwtProperties.java **/
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(&quot;spring.jwt&quot;)
@Getter
@Setter
@Component
public class JwtProperties {
    private String issuer;
    private String secretKey;
    private Long expiration;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(2) TokenProvider 서비스 작성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744267627741&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** TokenProvider.java **/
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;

import com.survey.server.model.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import org.springframework.security.core.Authentication;

@RequiredArgsConstructor
@Service
public class TokenProvider {
    private final JwtProperties jwtProperties;
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    /**
     * 주어진 사용자 정보로 새로운 JWT 토큰 생성
     *
     * @param userEmail 사용자 이메일 (사용자 구분할 unique한 값)
     * @param roles 사용자 권한 (ROLE_USER이 담긴 set)
     * @return 생성된 JWT 토큰 문자열
     */
    public String makeToken(String userEmail, Set&amp;lt;String&amp;gt; roles) {
        log.info(&quot;토큰 발행 프로세스 시작: email={}, roles={}&quot;, userEmail, roles);
        Date now = new Date();

        log.debug(&quot;토큰 정보: expiration={}&quot;, jwtProperties.getExpiration());
        Date validity = new Date(now.getTime() + jwtProperties.getExpiration());

        log.debug(&quot;토큰 리턴: issuer={}, secret_key={}&quot;, jwtProperties.getIssuer(), jwtProperties.getSecretKey());
        return Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)  //header type : JWT
                .setIssuer(jwtProperties.getIssuer())
                .setIssuedAt(now)
                .setSubject(userEmail)
                .claim(&quot;roles&quot;, roles)
                .setExpiration(validity)
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
                // signiture (secret key와 함께 해시값을 HS256 방식으로 암호화)
                .compact();
    }

    /**
     * 토큰 유효성 검증
     *
     * @param token 검증할 JWT 토큰
     * @return 토큰이 유효하면 true
     */
    public boolean isValidToken(String token) {
        try {
            Jwts.parser()
                    .setSigningKey(jwtProperties.getSecretKey())
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 유효한 토큰 기반으로 인증 정보 가져오기
     *
     * @param token 유효한 JWT 토큰
     * @return 사용자 정보와 기본 권한을 포함한 Authentication 객체
     */
    public Authentication getAuthentication(String token) {
        // 토큰에서 클레임 추출
        Claims claims = Jwts.parser()
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody();

        // 기본 권한 설정 (ROLE_USER)
        Set&amp;lt;SimpleGrantedAuthority&amp;gt; authorities = Collections
                .singleton(new SimpleGrantedAuthority((&quot;ROLE_USER&quot;)));

        // Authentication 객체 리턴
        return new UsernamePasswordAuthenticationToken(
                new org.springframework.security.core.userdetails.User( // 사용자 정보
                        claims.getSubject(),&quot;&quot;, authorities),  // - email, password(JWT 기반 인증에서 불필요), 권한정보(ROLE_USER)
                token, authorities); // 인증정보(token), 권한 리스트(authorities)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ method 01: makeToken() ] 토큰 생성: 아래와 같은 내용을 인코딩하여 토큰을 만든다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742792983409&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#header
{
    &quot;typ&quot;: &quot;JWT&quot;,
    &quot;alg&quot;: &quot;HS256&quot;
}

#payload
{
    &quot;iss&quot;: {jwtProperties에서 가져온 issuer},
    &quot;iat&quot;: {현재 시간: new Date() 값},
    &quot;exp&quot;: {method argument로 받은 expiry},
    &quot;roles&quot;: {사용자에게 부여할 접근 권한 set},
    &quot;sub&quot;: {subject로 user email 사용}
}

#signiture: jwProperties에서 가져온 secretKey&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ method 02: validToken() ] 토큰 유효성 검증&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;여기서 Jwts.parser()는 입력받은 토큰을 비밀값으로 복호화해서 토큰이 유효한지 확인하는 역할을 한다. setSigningKey로 입력받은 토큰을 복호화 할 secret key를 설정하고, parseClaimsJws(token)으로 인자인 'token'이 유효한 토큰인지를 검증한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ method 03: &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;getAuthentication()&amp;nbsp;&lt;/span&gt;] 토큰 기반으로 인증 정보 가져오기&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;token으로부터 claim 추출&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용자 권한 설정&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;부여되는 권한 : ROLE_USER (일반 사용자 권한, 관리자 권한은 ROLE_ADMIN으로 부여)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Set Collection Framework 사용 : 사용자가 여러개의 권한을 가질 수 있음을 의미. Set은 중복이 불가능하고 contains() 메서드가 있어 이용자 권한을 쉽게 저장하고 확인할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Collections.singleton() : authorization set에 변경이 불가능한 단일 요소만 추가. 따라서 authorization set에는 &quot;ROLE_USER&quot;만 포함 가능 =&amp;gt; 이용자는 ROLE_USER 권한만 가질 수 있음 (참고자료: &lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;https://ittrue.tistory.com/563&quot;&gt;https://ittrue.tistory.com/563&lt;/a&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;인증 정보 생성 (UsernamePasswordAuthenticationToken이라는 spring security 인증 객체 생성)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;첫번째 인자: User 객체 (사용자 정보)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;두번째 인자: token&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;세번째 인자: authorities (사용자 권한&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(3) Token Filter 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;각 요청마다 토큰이 유효한지 검증하고, 유효한 토큰만 context holder에 저장할 수 있도록 한다. 이때 context holder에 저장한다는 것은 추후 사용자가 인증이 필요한 페이지에 접근할 경우 인증된 사용자라는 것을 알 수 있도록 spring security에 알리는 것을 말한다. 추후 로그인이 필요한 서비스에서 사용될 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744271546411&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** TokenAuthenticationFilter.java **/
import com.survey.server.config.jwt.TokenProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final TokenProvider tokenProvider;
    private final static String HEADER_AUTHORIZATION = &quot;Authorization&quot;;
    private final static String TOKEN_PREFIX = &quot;Bearer &quot;;


    /**
     * 모든 요청에 대해 JWT 토큰을 검증하고 인증 처리를 수행
     *
     * @param request HTTP 요청
     * @param response HTTP 응답
     * @param filterChain 필터체인
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {

        // 요청 헤더에서 토큰 추출
        String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
        String token = (authorizationHeader != null &amp;amp;&amp;amp; authorizationHeader.startsWith(TOKEN_PREFIX))
                ? authorizationHeader.substring(TOKEN_PREFIX.length()) : null;

        // 유효한 토큰일 경우 context holder에 인증 정보 저장
        // - 현재 요청을 보낸 사용자가 인증되었음을 Spring Security에 알림
        if (tokenProvider.isValidToken(token)) {
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(4) Web Security Configuration 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;126&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;다음으로, 비밀번호 암호화와 페이지 접근 권한 설정을 위해 Web Security에 대한 Configuration을 작성해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;384&quot; data-start=&quot;202&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;먼저 페이지 접근 권한 설정 부분을 보면, 앞서 만든 Token Filter를 등록하여 인증이 필요한 모든 요청에 대해 JWT 토큰 기반 인증을 수행하도록 설정한다. 단, 로그인, 회원가입, 이메일 인증 등과 같이 인증 없이 접근 가능한 일부 페이지는 permitAll()을 통해 예외 처리되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;560&quot; data-start=&quot;389&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;또한, PasswordEncoder는 비밀번호를 DB에 평문으로 저장하는 것이 아닌 해시 값으로 안전하게 저장하기 위해 사용된다. 사용자가 로그인 시 입력한 비밀번호는 저장된 해시 값과 비교되어 인증되며, 이를 위해 passwordEncoder.matches() 메서드가 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744272333597&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** WebSecurityConfig.java **/
import com.survey.server.config.jwt.TokenAuthenticationFilter;
import com.survey.server.config.jwt.TokenProvider;
import com.survey.server.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
    private final TokenProvider tokenProvider;

    /**
     * 페이지 접근 권한 설정
     *  - 특정 HTTP 요청에 대해 웹기반 보안 구성
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                // CSRF 비활성화 (JWT에서는 불필요)
                .csrf(AbstractHttpConfigurer::disable)

                // JWT 기반 인증을 위한 Token Filter 추가
                .addFilterBefore(new TokenAuthenticationFilter(tokenProvider),
                        org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class)

                // 로그인 + 회원가입 관련 페이지 회원이 아니더라도 접근 허용
                .authorizeHttpRequests(auth -&amp;gt; auth
                        .requestMatchers(
                                &quot;/user/signup&quot;,
                                &quot;/user/login&quot;,
                                &quot;/user/verification/**&quot;
                        ).permitAll()
                        .anyRequest().authenticated())
                .build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder passwordEncoder, UserService userService) throws Exception {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userService);
        authProvider.setPasswordEncoder(passwordEncoder);
        return new ProviderManager(authProvider);
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. Entity 및 DTO 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(1) User Entity 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;User Entity의 구조는 다음과 같다. (실제로 지금 개발 중인 서비스의 ERD는 이와 조금 달랐으나, 편의상 지금 구현 중인 로그인/회원가입에 필요한 요소만으로 구성했다.)&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;컬럼명&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.9923%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자료형&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.7365%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;nullable&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.0853%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.8527%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특이사항&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;user_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.9923%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;int unsigned&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.7365%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;false&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.0853%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;일련번호, 기본키&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.8527%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자동 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;email&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.9923%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;varchar(255)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.7365%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;false&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.0853%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이용자 이메일&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.8527%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;중복 불가&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;user_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.9923%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;varchar(50)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.7365%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;false&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.0853%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용자 입력 아이디&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.8527%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;중복 불가&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;password_hash&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.9923%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;varchar(50)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.7365%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;fasle&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.0853%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;encoding된 사용자 비밀번호&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.8527%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;bcrytypt encoder 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;created_at&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.9923%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DATE&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.7365%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;false&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.0853%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;회원가입일 (이용자 생성일)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.8527%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자동 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;privacy_agree&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.9923%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;boolean&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.7365%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;fasle&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.0853%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;개인정보보호 동의 여부&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.8527%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;false 값일 수 없음 (가입 불가)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;marketing_agree&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.9923%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;boolean&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.7365%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;false&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.0853%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;마케팅수신 동의 여부&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.8527%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;선택 약관&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;자동생성이라고 표기된 id는 @GenerateValue 어노테이션으로 일련번호가 자동 생성되고, created_at의 경우에도 @PrePersist 어노테이션과 함께 사용된 onCreate method로 자동으로 회원가입 시의 날짜가 부여된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;그리고 User Entity는 UserDetails를 implement 한다. UserDetails를 Spring Security에서 사용자의 인증 정보를 담아 두는 interface이다. 해당 객체를 토앻 인증 정보를 가져오려면 필수로 몇가지 method들을 override해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744461224090&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** User.java **/
import com.survey.server.constant.SEX;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Date;
import java.util.List;

@Entity
@Table(name=&quot;users&quot;)
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name=&quot;user_id&quot;, updatable=false)
    private Long userId;

    @Column(name=&quot;user_name&quot;, nullable = false)
    private String userName;

    @Column(name=&quot;email&quot;, nullable=false, unique=true)
    private String email;

    @Column(name=&quot;user_ident&quot;, nullable=false, unique=true)
    private String userIdent;

    @Column(name=&quot;password_hash&quot;, nullable=false)
    private String passwordHash;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name=&quot;created_at&quot;, nullable=false, updatable=false)
    private Date createdAt;

    @Column(name=&quot;privacy_agree&quot;, nullable=false)
    private Boolean privacyAgree;

    @Column(name=&quot;marketing_agree&quot;, nullable=false)
    private Boolean marketingAgree;

    // 비밀번호 변경
    public void updatePassword(String newPasswordHash) {
        this.passwordHash = newPasswordHash;
    }

    // createdAt 자동 생성
    @PrePersist
    protected void onCreate() {
        this.createdAt = new Date();
    }

    // 사용자가 가지고 있는 권한 목록 반환
    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        return List.of(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;));
    }

    // 사용자 이름 반환
    @Override
    public String getUsername() {
        return userName;
    }

    // encoding 된 비밀번호 반환
    @Override
    public String getPassword() {
        return passwordHash;
    }

    // 계정 만료 여부 반환
    @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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(2) SignupRequestDTO 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;DTO의 경우 너무 많은 파일이 존재하기 때문에, 기본적으로 DTO를 쓸 때 신경 썼던 부분이 모두 담겨있는 SignupRequestDTO만 기록하고 넘어가려 한다.&amp;nbsp;DTO들에서 값의 패턴이나 조건을 확인하는 어노테이션을 많이 사용했다. 형식에 맞는 값이 아니라면 DTO 생성에서부터 exception을 발생시켜 다음 단계를 진행할 수 없도록 코드를 작성했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;우선 전반적으로 빈 값이면 안되는 값들에 대해 &lt;u&gt;@NotBlank&lt;/u&gt; 어노테이션을 사용했다. 무조건 입력되어야 하는 값들에 사용할 수 있는 어노테이션이 세가지 있는데, 그 내용은 아래와 같다. 결론부터 말하자면 나는 가장 엄격하게 입력값을 확인하는 어노테이션을 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 70px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 15.814%; height: 22px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 16.8604%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Null 값 허용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.6744%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;빈칸 허용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.721%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;공백만 있는 값 허용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.9302%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 15.814%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@NotNull&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.8604%; height: 16px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.6744%; height: 16px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;O&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.721%; height: 16px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;O&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.9302%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Null값인지 확인&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 15.814%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@NotEmpty&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.8604%; height: 16px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.6744%; height: 16px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.721%; height: 16px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;O&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.9302%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;입력값의 길이를 확인&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 15.814%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@NotBlank&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.8604%; height: 16px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.6744%; height: 16px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.721%; height: 16px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.9302%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;공백을 제거한 입력값의 길이를 확인&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;비밀번호 형식의 경우 &lt;u&gt;@Pattern&lt;/u&gt; 어노테이션을 사용했다. regexp가 지켜저야 하는 비밀번호 형식에 해당하는데, 나는 8자리 이상의 숫자, 알파벳, 특수문자 중 모두를 조합한 비밀번호인지 확인했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;그리고 privacyArgee, 즉 개인정보이용동의 항목 같은 경우, 동의하지 않으면 회원가입을 진행할 수 없다. 따라서 이 경우 &lt;u&gt;@AssertTrue&lt;/u&gt; 어노테이션을 활용하여 true 값이 아니면 exception을 발생시켰다. 비슷한 로직으로 userPassword와 passwordConfirm, 즉 비밀번호 입력란과 비밀번호 확인란의 값이 같은지, DTO내 method인&amp;nbsp; isPasswordMatching()에 @AssertTrue 어노테이션을 달아 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744462528212&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** SignupRequestDTO.java **/
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Getter;
import jakarta.validation.constraints.NotBlank;

@AllArgsConstructor
@Getter
public class SignupRequestDTO {
    @NotBlank(message = &quot;user_name cannot be empty&quot;)
    String userName;
    @NotBlank(message = &quot;user_email cannot be empty&quot;)
    String userEmail;
    @NotBlank(message = &quot;user_ident cannot be empty&quot;)
    String userIdent;

    @NotBlank(message = &quot;user_password cannot be empty&quot;)
    @Pattern(
            regexp = &quot;^(?=.*[A-Za-z].*)(?=.*\\d.*|.*[^A-Za-z0-9].*).{8,}$&quot;,
            message = &quot;Password must be at least 8 characters long and contain at least two of the following: letters, numbers, or special characters&quot;
    )
    String userPassword;
    String passwordConfirm;
    @AssertTrue(message = &quot;Passwords do not match&quot;)
    public boolean isPasswordMatching() {
        return userPassword.equals(passwordConfirm);
    }

    @AssertTrue(message = &quot;user should agree with privacy agreement&quot;)
    Boolean privacyAgree;
    Boolean marketingAgree;

    @AssertTrue(message = &quot;user should check ident duplication first&quot;)
    Boolean identConfirmed;
    @AssertTrue(message = &quot;user should verify email first&quot;)
    Boolean emailVerified;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. User 관련 주요 로직 작성 (controller, service, repository)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(1) User Controller 작성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744464605924&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** UserController.java **/
import com.survey.server.dto.*;
import com.survey.server.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.NoSuchElementException;

@RequiredArgsConstructor
@RestController
@RequestMapping(&quot;/user&quot;)
public class UserController {
    private final UserService userService;

    /**
     * 회원 가입 endpoint
     *
     * @param request 회원가입 요청 DTO
     * @return 회원가입 응답 DTO를 포함한 ResponseEntity
     */
    @PostMapping(&quot;/signup&quot;)
    public ResponseEntity&amp;lt;SignupResponseDTO&amp;gt; signup(@RequestBody SignupRequestDTO request) {
        try {
            SignupResponseDTO response = userService.signup(request);
            return ResponseEntity.status(HttpStatus.CREATED).body(response);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * 아이디 중복확인 endpoint
     *
     * @param request 아이디 중복확인 요청 DTO
     * @return 중복 확인 결과 Boolean 값 (중복되지 않을 시, 즉 아이디를 사용 가능한 경우 true)
     */
    @PostMapping(&quot;/signup/id&quot;)
    public ResponseEntity&amp;lt;Boolean&amp;gt; confirmIdent(@RequestBody IdentConfirmDTO request) {
        try {
            Boolean result = userService.confirmIdent(request);
            return ResponseEntity.ok(result);
        } catch (IllegalStateException e) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body(false);
        }
    }

    /**
     * 로그인 endpoint
     *
     * @param request 로그인 요청 DTO
     * @return 로그인 응답 DTO를 포함한 ResponseEntity
     */
    @PostMapping(&quot;/login&quot;)
    public ResponseEntity&amp;lt;LoginResponseDTO&amp;gt; login(@RequestBody @Validated LoginRequestDTO request) {
        try {
            LoginResponseDTO response = userService.login(request);
            return ResponseEntity.ok(response);
        } catch (IllegalStateException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * 아이디 찾기 endpoint
     *
     * @param request 아이디 찾기 요청 DTO
     * @return 아이디 찾기 응답 DTO를 포함한 ResponseEntity
     */
    @PostMapping(&quot;/ident&quot;)
    public ResponseEntity&amp;lt;FindIdentResponseDTO&amp;gt; findIdent(@RequestBody FindIdentRequestDTO request) {
        try {
            FindIdentResponseDTO response = userService.findIdent(request);
            return ResponseEntity.ok(response);
        } catch (IllegalStateException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * 비밀번호 찾기 endpoint
     *
     * @param request 비밀번호 찾기 요청 DTO
     * @return 회원정보와 일치하는 회원의 존재여부 (회원 정보 + 인증 번호 모두 맞아야 true)
     */
    @PostMapping(&quot;/password&quot;)
    public ResponseEntity&amp;lt;Boolean&amp;gt; findPassword(@RequestBody FindPasswordRequestDTO request) {
        try {
            Boolean result = userService.findPassword(request);
            return ResponseEntity.ok(result);
        } catch (IllegalStateException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * 인증 메일 전송 endpoint
     *
     * @param sendDTO 이메일 전송 요청 DTO
     * @return 이메일 전송 결과 String
     */
    @PostMapping(&quot;/verification/email&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; sendVerificationEmail(@RequestBody VerificationEmailSendDTO sendDTO) {
        try {
            userService.sendVerificationEmail(sendDTO);
            return ResponseEntity.ok(&quot;Verification email sent&quot;);
        } catch (IllegalStateException e) { // 회원가입 시 이메일 중복
            return ResponseEntity.status(HttpStatus.CONFLICT).body(&quot;signup: existing email&quot;);
        } catch (Exception e) {
            return ResponseEntity.internalServerError().build();
        }
    }

    /**
     * 인증번호 확인 endpoint
     *
     * @param verifyDTO 인증번호 확인 요청 DTO
     * @return 인증번호 확인 결과 Boolean (번호 일치할 경우 true)
     * 이메일 존재하지 않을 경우 UNAUTHORIZED(401)
     */
    @PostMapping(&quot;/verification&quot;)
    public ResponseEntity&amp;lt;Boolean&amp;gt; verifyCode(@RequestBody CodeVerifyDTO verifyDTO) {
        try {
            Boolean result = userService.verifycode(verifyDTO);
            return ResponseEntity.ok(result);
        } catch (NoSuchElementException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * 비밀번호 변경 endpoint
     *
     * @param passwordUpdateDTO 비밀번호 변경 요청 DTO
     * @return 비밀번호 변경 결과 String
     */
    @PostMapping(&quot;/password/new&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; updatePassword(@RequestBody PasswordUpdateDTO passwordUpdateDTO) {
        try {
            userService.updatePassword(passwordUpdateDTO);
            return ResponseEntity.ok(&quot;Password updated&quot;);
        } catch (IllegalStateException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        } catch (Exception e) {
            return ResponseEntity.internalServerError().build();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(2) User Service 작성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744465247610&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** UserService.java **/
import com.survey.server.config.jwt.TokenProvider;
import com.survey.server.dto.*;
import com.survey.server.repository.UserRepository;
import com.survey.server.model.User;
import lombok.RequiredArgsConstructor;
import org.apache.logging.log4j.util.InternalException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final UserRepository userRepository;
    private final TokenProvider tokenProvider;
    private final MailService mailService;

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    /**
     * 회원가입 요청을 처리하여 사용자 정보를 저장하고 반환
     *
     * @param request 회원가입 요청 DTO
     * @return 회원가입 응답 DTO
     */
    public SignupResponseDTO signup(SignupRequestDTO request) {
        log.info(&quot;회원가입 프로세스 시작: ident={}, email={}&quot;, request.getUserIdent(), request.getUserEmail());

        String passwordHash = bCryptPasswordEncoder.encode(request.getUserPassword());
        log.debug(&quot;회원가입: 비밀번호 encoding 완료&quot;);
        User user = User.builder()
                .userName(request.getUserName())
                .userIdent(request.getUserIdent())
                .email(request.getUserEmail())
                .passwordHash(passwordHash)
                .point(0L)
                .privacyAgree(request.getPrivacyAgree())
                .marketingAgree(request.getMarketingAgree())
                .build();
        log.debug(&quot;회원가입: user entity 생성 완료&quot;);

        try {
            userRepository.save(user);
            log.info(&quot;사용자 정보 저장 완료: ident={}, name={}&quot;, request.getUserIdent(), request.getUserName());
        } catch (Exception e) {
            log.error(&quot;회원가입 실패: ident={}, error={}&quot;, request.getUserIdent(), e.getMessage());
            throw new RuntimeException(&quot;회원 정보 저장 실패&quot;);
        }

        return new SignupResponseDTO(
                user.getUserIdent(),
                user.getUsername()
        );
    }

    /**
     * 아이디 중복확인하고 결과 반환
     *
     * @param request 아이디 중복확인 요청 DTO
     * @return 중복되지 않을 시 true, 중복되는 경우 exception 발생
     */
    public Boolean confirmIdent(IdentConfirmDTO request) {
        log.info(&quot;아이디 중복확인 시작: ident={}&quot;, request.getUserIdent());

        if (!userRepository.existsByUserIdent(request.getUserIdent())) {
            log.info(&quot;사용 가능한 아이디: ident={}&quot;, request.getUserIdent());
            return true;
        } else {
            log.info(&quot;중복된 아이디: ident={}&quot;, request.getUserIdent());
            throw new IllegalStateException(&quot;이미 사용하고 있는 아이디입니다.&quot;);
        }
    }

    /**
     * 로그인 처리하고 결과 반환
     * @param request 로그인 요청 DTO
     * @return 로그인 응답 DTO
     */
    public LoginResponseDTO login(LoginRequestDTO request) {
        log.info(&quot;로그인 시도: ident={}&quot;, request.getUserIdent());

        Optional&amp;lt;User&amp;gt; optionalUser = userRepository.findByUserIdent(request.getUserIdent());
        if (optionalUser.isEmpty() ||
                !bCryptPasswordEncoder.matches(request.getUserPassword(), optionalUser.get().getPasswordHash())) {
            log.warn(&quot;로그인 실패: ident={}, reason=invalid_credentials&quot;, request.getUserIdent());
            throw new IllegalStateException(&quot;로그인 실패: invalid_credentials&quot;);
        }
        log.debug(&quot;로그인 조건 충족 (valid id and password)&quot;);

        // 토큰 생성
        User user = optionalUser.get();
        Set&amp;lt;String&amp;gt; roles = user.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toSet());

        log.debug(&quot;토큰 생성: ident={}, roles={}&quot;, request.getUserIdent(), roles);
        String token = tokenProvider.makeToken(user.getEmail(), roles);

        log.info(&quot;로그인 성공: ident={}&quot;, request.getUserIdent());
        return new LoginResponseDTO(
                token,
                user.getUserIdent(),
                roles
        );
    }

    /**
     * 아이디 찾기 (인증 코드 확인하고 아이디 반환)
     * @param request 아이디 찾기 요청 DTO
     * @return 아이디 찾기 응답 DTO
     * @throws Exception (verification code 확인하며 발생한 exception throw)
     */
    public FindIdentResponseDTO findIdent(FindIdentRequestDTO request) throws Exception {
        log.info(&quot;아이디 찾기 시작: email={}&quot;, request.getUserEmail());

        User user = userRepository.findByEmail(request.getUserEmail())
                .orElseThrow(() -&amp;gt; new IllegalStateException(&quot;Invalid user credentials&quot;));
        log.debug(&quot;아이디 찾기 대상 user 찾기 성공 (valid email)&quot;);
        if (!user.getUsername().equals(request.getUserName())) {
            log.warn(&quot;아이디 찾기 실패: email={}, reason=invalid_credentials&quot;, request.getUserEmail());
            throw new IllegalStateException(&quot;Invalid user credentials&quot;);
        }

        // 인증 번호 확인
        if(!mailService.verifycode(request.getUserEmail(), request.getVerificationCode())) {
            log.warn(&quot;아이디 찾기 실패: email={}, reason=invalid_verification_code&quot;, request.getUserEmail());
            throw new IllegalStateException(&quot;Invalid verification code&quot;);
        }

        log.debug(&quot;아이디 찾기 성공: email={}, ident={}&quot;, user.getEmail(), user.getUserIdent());
        return new FindIdentResponseDTO(
                user.getUserIdent()
        );
    }

    /**
     * 비밀번호 찾기 (비밀번호 변경으로 넘어가기 전 확인)
     * @param request 비밀번호 찾기 요청 DTO
     * @return true면 비밀번호 변경으로 이동 가능 (조건 충족)
     * @throws Exception verifyCode에서 받은 exception throw
     */
    public Boolean findPassword(FindPasswordRequestDTO request) throws Exception {
        log.info(&quot;비밀번호 찾기 시작: email={}&quot;, request.getUserEmail());
        User user = userRepository.findByEmail(request.getUserEmail())
                .orElseThrow(() -&amp;gt; new IllegalStateException(&quot;Invalid user credentials&quot;));
        log.debug(&quot;비밀번호 찾기 대상 user 찾기 성공 (valid email)&quot;);
        if (!user.getUsername().equals(request.getUserName())) {
            log.warn(&quot;비밀번호 찾기 실패: email={}, reason=invalid_credentials&quot;, request.getUserEmail());
            throw new IllegalStateException(&quot;Invalid user credentials&quot;);
        }

        // 인증 번호 확인
        Boolean isValidCode = mailService.verifycode(request.getUserEmail(), request.getVerificationCode());
        if (isValidCode) {
            log.info(&quot;비밀번호 찾기 성공: email={}&quot;, request.getUserEmail());
        } else {
            log.info(&quot;비밀번호 찾기 실패: email={}, reason=invalid_verification_code&quot;, request.getUserEmail());
            throw new IllegalStateException(&quot;Invalid user credentials&quot;);
        }
        return isValidCode;
    }

    /**
     * 인증 코드 확인
     * @param verifyDTO 인증 코드 확인 요청 DTO
     * @return 인증 코드가 일치하면 true, 아니면 false
     */
    public Boolean verifycode(CodeVerifyDTO verifyDTO) {
        log.debug(&quot;인증코드 확인: email={}&quot;, verifyDTO.getUserEmail()); //mailService에서 로깅
        return mailService.verifycode(verifyDTO.getUserEmail(), verifyDTO.getVerificationCode());
    }

    /**
     * 인증 메일 전송
     * @param sendDTO 인증 메일 전송 요청 DTO
     * @throws Exception 메일 전송 시 발생한 exception throw
     */
    public void sendVerificationEmail(VerificationEmailSendDTO sendDTO) throws Exception {
        log.info(&quot;인증 메일 전송 시작: email={}&quot;, sendDTO.getUserEmail());
        if (sendDTO.getRequestType() == VerificationEmailSendDTO.RequestType.SIGNUP
                &amp;amp;&amp;amp; userRepository.existsByEmail(sendDTO.getUserEmail())) {
            log.warn(&quot;인증 메일 전송 실패: email={}, reason=invalid_credentials&quot;, sendDTO.getUserEmail());
            throw new IllegalStateException(&quot;existing email&quot;);
        }

        if (!mailService.sendVerificationEmail(sendDTO.getUserEmail())) {
            log.error(&quot;인증 메일 전송 실패: email={]&quot;, sendDTO.getUserEmail());
            throw new RuntimeException(&quot;failed to send verification email&quot;);
        }
        log.info(&quot;인증 메일 전송 성공: email={}&quot;, sendDTO.getUserEmail());
    }

    /**
     * 비밀번호 변경
     * @param request 비밀번호 변경 요청 DTO
     */
    public void updatePassword(PasswordUpdateDTO request) {
        log.info(&quot;비밀번호 변경 시작: email={}&quot;, request.getUserEmail());

        User user = userRepository.findByEmail(request.getUserEmail())
                .orElseThrow(() -&amp;gt; new IllegalStateException(&quot;Invalid user credentials&quot;));
        log.debug(&quot;비밀번호 변경 대상 user 찾기 성공(valid email)&quot;);

        String passwordHash = bCryptPasswordEncoder.encode(request.getUserPassword());
        user.updatePassword(passwordHash);
        userRepository.save(user);
        log.info(&quot;비밀번호 변경 성공&quot;);
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Fetch user by username from the database
        User user = userRepository.findByUserName(username)
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;User not found&quot;));

        // Return a UserDetails object based on your User class
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                user.getAuthorities()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(3) User Repository 작성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744465344021&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** UserRepository.java **/
import com.survey.server.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
    Optional&amp;lt;User&amp;gt; findByEmail(String userEmail);
    Optional&amp;lt;User&amp;gt; findByUserIdent(String userIdent);

    Boolean existsByEmail(String userEmail);
    Boolean existsByUserIdent(String userIdent);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. 인증번호 관련 주요 로직 작성(service, repository)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;인증번호 관련 서비스는 아래 글에 보다 자세히 기록했다.&lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://programming-diary-ina.tistory.com/120&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;&amp;nbsp;https://programming-diary-ina.tistory.com/120&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1744465988940&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/Java] JavaMailSender로 인증 메일 전송하기&quot; data-og-description=&quot;회원가입 및 로그인 기능을 구현하면서, 인증코드를 담은 메일을 보내는 기능을 구현해야 했다. JavaMailSender를 이용하면 어렵지 않게 구현할 수 있다. 크게 아래 두 가지 기능을 구현해 보았다.1. &quot; data-og-host=&quot;programming-diary-ina.tistory.com&quot; data-og-source-url=&quot;https://programming-diary-ina.tistory.com/120&quot; data-og-url=&quot;https://programming-diary-ina.tistory.com/120&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KniPc/hyYEFCGXaD/LgBmS5z7ykesBb5kzSDeYk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/ddZajg/hyYCgKrFE2/3FEmMcxtvaEv6I0rQkmRak/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://programming-diary-ina.tistory.com/120&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://programming-diary-ina.tistory.com/120&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KniPc/hyYEFCGXaD/LgBmS5z7ykesBb5kzSDeYk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/ddZajg/hyYCgKrFE2/3FEmMcxtvaEv6I0rQkmRak/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;[SpringBoot/Java] JavaMailSender로 인증 메일 전송하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;회원가입 및 로그인 기능을 구현하면서, 인증코드를 담은 메일을 보내는 기능을 구현해야 했다. JavaMailSender를 이용하면 어렵지 않게 구현할 수 있다. 크게 아래 두 가지 기능을 구현해 보았다.1.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programming-diary-ina.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>웹 백엔드 개발/Spring Boot</category>
      <author>iinana</author>
      <guid isPermaLink="true">https://programming-diary-ina.tistory.com/126</guid>
      <comments>https://programming-diary-ina.tistory.com/126#entry126comment</comments>
      <pubDate>Sat, 12 Apr 2025 22:22:27 +0900</pubDate>
    </item>
    <item>
      <title>[운영체제 실습] thread 이용해보기</title>
      <link>https://programming-diary-ina.tistory.com/125</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ffffff; background-color: #1a5490; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&amp;nbsp;실습 명세&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. Implement a simple counter using threads.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. Calculate the time difference between a version using a single process and a version using multiple threads. (consider the time difference based on the number of threads.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. Determine how the results differ when using locks versus not using locks with threads.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4. Advanced: Implement a producer-consumer pattern using condition variables.&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #1a5490; color: #ffffff;&quot;&gt;&amp;nbsp;POSIX thread (pthread) 함수&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. pthread API&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(1)&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void *(start_routine)(void *), void *arg);&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(역할) 새로운 thread 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) thread : 새로운 thread의 ID (out)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) attr: thread attribute configuration (기본 속성 사용 시, NULL)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) start_routine: thread가 해야 하는 일&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) arg: arguments pass to start_routine&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(return) success=0, error=error_num&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(2) int pthread_join(pthread_t thread, void** ret_val);&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(역할) 자식 thread가 종료될 때까지 기다리는 함수 (mater thread는 하나의 worker thread 만들고 끝날 때까지 기다렸다가 다음 일을 한다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) thread_t: terminate 될 target thread의 ID&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) ret_val: terminated thread의 return value (out)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(return) success=0&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(3) void pthread_exit(void* ret_val);&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(역할) terminates the thread (thread를 시작하는 함수의 return call은 암묵적으로 pthread_exit을 호출)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) ret_val: terminated thread의 return value.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. MUTEX&lt;/b&gt;&lt;b&gt; (critical section을 사용하기 위해 보호해 주는 역할)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(1) int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(역할) initialize a mutex object&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) mutex: mutex가 초기화될 pointer (in/out)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) attr: mutex attributes (default=NULL)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(return) succes=0, error=error_num&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(2) int pthread_mutex_lock(pthread_mutex_t *mutex);&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(역할) lock a mutex&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) mutex: lock을 걸 mutex pointer&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(return) success=0, error=error_num&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(3) int pthread_mutex_unlock(pthread_mutex_t *mutex);&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(역할) unlock a mutex&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) mutex: unlock할 mutex pointer&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(return) success=0, error=error_num&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. condition variable (특정 조건을 만족하기를 기다리는 변수)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(1) int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(역할) condition variable 초기화 for inter-thread signaling (thread 간 signaling)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) cond: 초기화될 condition variable의 pointer (in/out)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) attr: condition variable attributes&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(return) success=0, error=error_num&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(2) int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(역할) condition variable이 signal 될 때까지 기다림&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) cond: wait하고 있는 condition variable의 pointer (in/out)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) mutex: condition을 protect 하고 있는 mutex의 pointer (in/out)&lt;br /&gt;&amp;rarr; 호출 시 mutex를 자동으로 unlock하고, 조건이 충족되어 signal을 받으면 mutex를 다시 lock (deadlock 방지)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(return) success=0, error=error_num&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(3) int pthread_cond_signal(pthread_cond_t *cond);&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(역할) condition variable에 기다리고 있는 thread 하나에 signaling (wake up at least one thread waiting on the condition)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) cond: signal 할 conditon variable의 pointer&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(return) success=0, error=error_num&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(4) int pthread_cond_broadcast(pthread_cond_t *cond);&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(역할) 해당 condtion variable에서 기다리는 모든 thread를 signal&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(param) cond: broadcas 할 conditon variable의 pointer&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(return) success=0, error=error_num&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(5) 주의할 점&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;condition variable도 공유되는 변수이므로 &lt;u&gt;mutually exclusive&lt;/u&gt;해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;반드시 if문이 아닌 while문으로 조건을 확인해야 한다&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(ex) if문 조건을 확인할 때는 wait 조건을 충족하지 않았는데, 중간에 interrupt가 발생되어 wait 조건을 충족하게 됨&amp;nbsp; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; =&amp;gt; wait을 해야 하지만 하지 않는 상황 발생&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. pthread를 linux 환경에서 실행하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;아래 명령어를 통해 리눅스에서 pthread를 사용한 파일을 컴파일할 수 있다. window 환경에서는 pthread.h를 사용할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744360913031&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gcc -o counter counter.c -lpthread&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #1a5490; color: #ffffff; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&amp;nbsp;Implementaion&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. simple counter 구현 (lock이 없는 버전)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744361056569&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

#define NUM_THREADS 4
#define NUM_INCREMENTS 1000000

int counter = 0;

void* increment(void* arg) {
    for (int i = 0; i &amp;lt; NUM_INCREMENTS; i++) {
        counter++;
    }
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];

    // NUM_THREADS 개의 thread 생성
    for (int i = 0; i &amp;lt; NUM_THREADS; i++) {
        pthread_create(&amp;amp;threads[i], NULL, increment, NULL);
    }

    // 모두 실행할 때까지 기다림
    for (int i = 0; i &amp;lt; NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    // Expected counter보다 actual value가 과소하게 출력됨
    printf(&quot;Expected counter: %d\n&quot;, NUM_THREADS * NUM_INCREMENTS);
    printf(&quot;Actual value:     %d\n&quot;, counter);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;결과는 아래와 같다. Race Condition의 발생으로 counter의 기댓값보다 실제값이 더 작은 것을 확인할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744361086523&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./counter_no_lock
Expected counter: 4000000
Actual value:     1775492&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. single process를 사용할 때와 thread를 사용할 때의 시간 차이&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;아래 코드로 NUM_THREADS와 NUM_LOOP만 수정해줘 가면서 시간 차이를 테스트해 봤다. race condition 없이 순수 실행 시간만 비교하기 위해 counter로 구현하는 대신 그냥 loop만 돌렸다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744436449799&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys/time.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

#define NUM_THREADS 3
#define NUM_LOOP 100000000

long get_elapsed_usec(struct timeval start, struct timeval end) {
    return (end.tv_sec - start.tv_sec) * 1000000L + (end.tv_usec - start.tv_usec);
}

void* loop(void* arg) {
    for (int i = 0; i &amp;lt; NUM_LOOP; i++);
    return NULL;
}

long with_thread() {
    pthread_t threads[NUM_THREADS];
    struct timeval start, end;

    gettimeofday(&amp;amp;start, NULL);
    // NUM_THREADS 개의 thread 생성
    for (int i = 0; i &amp;lt; NUM_THREADS; i++) {
        pthread_create(&amp;amp;threads[i], NULL, loop, NULL);
    }

    // 모두 실행할 때까지 기다림
    for (int i = 0; i &amp;lt; NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    gettimeofday(&amp;amp;end, NULL);
    return get_elapsed_usec(start, end); 
}

long without_thread() {
    struct timeval start, end;

    gettimeofday(&amp;amp;start, NULL);
    for (int i = 0; i &amp;lt; NUM_THREADS; i++) {
        loop(NULL);
    }
    
    gettimeofday(&amp;amp;end, NULL);
    return get_elapsed_usec(start, end); 
}

int main() {
    printf(&quot;duration using thread:   %8ld\n&quot;, with_thread());
    printf(&quot;duration without thread: %8ld\n&quot;, without_thread());    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;첫번째로, &lt;u&gt;NUM_THREAD=2&lt;/u&gt;, &lt;u&gt;NUM_LOOP=1,000&lt;/u&gt;&amp;nbsp;일 때의 실행 결과이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744436543299&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./measure_time
duration using thread:        474
duration without thread:        5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;보다시피 thread를 사용했을 때 오히려 더 많은 시간이 걸리는 것을 알 수 있다. 보통 thread를 사용하면 더 효율적으로, 더 짧은 시간으로 실행할 수 있을 것이라는 게 배웠던 내용인데, 오히려 thread로 사용하는 것이 더 오래 걸린다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;다양한 원인이 있을 수 있지만, 설정한 thread 수가 2개로 아주 적고, 반복 횟수가 작아, 한 thread 안에서의 작업이 그렇게 많지 않을 것이다. 그래서 오히려 thread로 인해 줄일 수 있는 시간보다 context switching으로 인해 늘어나는 시간이 더 커져서 발생하는 현상이라고 볼 수 있다. (&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;multi thread를 사용한다는 것은 커널이 thread를 계속 바꿔가면서 써야 한다는 것이기 때문에 context switching 비용이 발생한다.)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;따라서 다음은 우선 &lt;u&gt;NUM_LOOP=100,000,000&lt;/u&gt;으로 충분한 수로 늘려서 다시 실행해 봤다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744437712837&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./measure_time
duration using thread:     279984
duration without thread:   517649&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;이제 thread를 사용하는 편의 시간이 더 짧아졌다. 즉, contex switching 비용보다 thread로 절약할 수 있는 시간이 더 커졌다는 의미이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;&amp;nbsp;마지막으로 여기서 &lt;u&gt;NUM_THREAD=4&lt;/u&gt;로 thread의 개수만 늘려보았다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744437940833&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./measure_time
duration using thread:     362409
duration without thread:  1031092&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;thread를 사용한 것과 사용하지 않은 시간 간의 더 명확한 차이가 발생하는 것을 볼 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;u&gt;결론적으로 하려는 일이 충분히 오랜 시간이 걸리는 경우 thread를 사용하는 것이 더 유리하다.&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. mutex lock을 적용한 counter 구현&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;처음에 구현한 simple counter에서 increament 함수에서 crtical region에 해당하는 counter 증가 부분 앞뒤로 lock을 걸어주고 풀어주기만 하면 된다. (main 함수는 같아서 생략했다.)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744363349763&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

#define NUM_THREADS 4
#define NUM_INCREMENTS 1000000

int counter = 0;
pthread_mutex_t lock;

void* increment(void* arg) {
    for (int i = 0; i &amp;lt; NUM_INCREMENTS; i++) {
        pthread_mutex_lock(&amp;amp;lock);
        counter++;
        pthread_mutex_unlock(&amp;amp;lock);
    }
    return NULL;
}

// 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;결과는 아래와 같다. lock을 걸지 않았을 때는 기댓값과 실제값이 달랐지만, lock을 걸고 나니 기댓값과 실제값이 똑같은 것을 알 수 있다. 즉, Race Condition으로 인한 문제가 해결되었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744363470317&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./counter_with_lock
Expected counter: 4000000
Actual value:     4000000&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4. producer-consumer pattern 구현&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;producer-consumer pattern을 아래와 같이 구현해 봤다. printf와 main 함수는 모두 디버깅 로그를 기록하기 위함이다. (결과 확인용)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744450171814&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#define BUFFER_SIZE 5
#define LOOP 10

int count = 0;
int in = 0, out = 0;
int buffer[BUFFER_SIZE];

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
// buffer 비어있을 때 wait 시킬 condtion variable
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;
// buffer 가득 찼을 때 wait 시킬 condition variable
pthread_cond_t full = PTHREAD_COND_INITIALIZER;

void put(int val) {
    buffer[in] = val;
    in = (in + 1) % BUFFER_SIZE;
    count++;
}

int get() {
   int val = buffer[out]; 
   out = (out + 1) % BUFFER_SIZE;
   count--;
   return val;
}

void* producer(void* arg) {
    for (int i = 0; i &amp;lt; LOOP; i++) {
        pthread_mutex_lock(&amp;amp;lock);
        while (count &amp;gt;= BUFFER_SIZE) {
            printf(&quot;producer: buffer full...\n&quot;);
            pthread_cond_wait(&amp;amp;full, &amp;amp;lock);
            // consumer가 element 하나 소비하고 signal 해줄 때까지 wait
        }

        put(rand() % 100);
        printf(&quot;producer: in=%d, count=%d\n&quot;, in, count);

        pthread_cond_signal(&amp;amp;empty);
        // consumer에 새로운 element 넣었다고 signal

        pthread_mutex_unlock(&amp;amp;lock);
    }
}

void* consumer(void* arg) {
    for (int i = 0; i &amp;lt; LOOP; i++) {
        pthread_mutex_lock(&amp;amp;lock);
        while (count &amp;lt;= 0) {
            printf(&quot;consumer: buffer empty...\n&quot;);
            pthread_cond_wait(&amp;amp;empty, &amp;amp;lock);
            // producer가 새로운 element 넣고 signal 해줄 때까지 wait
        }

        printf(&quot;consumer: get value: %d\n&quot;, get());

        pthread_cond_signal(&amp;amp;full);
        // producer에 element 하나 소비했다고 signal
        pthread_mutex_unlock(&amp;amp;lock);
    }
}

int main() {
    pthread_t prod, cons;

    pthread_create(&amp;amp;prod, NULL, producer, NULL);
    pthread_create(&amp;amp;cons, NULL, consumer, NULL);

    pthread_join(prod, NULL);
    pthread_join(cons, NULL);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;결과는 아래와 같이 나왔다. 결과가 너무 길어 뒷부분은 생략했다. 디버깅 메시지가 출력되는 것을 보면, 처음에 producer가 지정된 buffer size(10)만큼 모두 채워서 full이 출력되고 더 이상 produer가 동작하지 못한다. 그리고 consumer가 동작하고, full condition variable에 signal을 주고 나서 다시 produer가 동작 가능해진 것도 볼 수 있다. consumer의 동작도 마찬가지이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744450349608&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./prod_cons
producer: in=1, count=1  
producer: in=2, count=2  
producer: in=3, count=3  
producer: in=4, count=4  
producer: in=0, count=5  
producer: buffer full... 
consumer: get value: 83  
consumer: get value: 86  
consumer: get value: 77  
consumer: get value: 15  
consumer: get value: 93  
consumer: buffer empty...
producer: in=1, count=1  
producer: in=2, count=2  
producer: in=3, count=3  
producer: in=4, count=4
producer: in=0, count=5
consumer: get value: 35
consumer: get value: 86
consumer: get value: 92
consumer: get value: 49
consumer: get value: 21&lt;/code&gt;&lt;/pre&gt;</description>
      <category>학교 공부/운영체제</category>
      <author>iinana</author>
      <guid isPermaLink="true">https://programming-diary-ina.tistory.com/125</guid>
      <comments>https://programming-diary-ina.tistory.com/125#entry125comment</comments>
      <pubDate>Sat, 12 Apr 2025 18:37:01 +0900</pubDate>
    </item>
    <item>
      <title>[논문 Review]  Development of an Automated ESG Document Review System using Ensemble-Based OCR and RAG Technologies</title>
      <link>https://programming-diary-ina.tistory.com/124</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ESG 관련 내용은 제하고, 졸업 프로젝트에 필요한 RAG 기술 관련 내용을 중심으로 정리했다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Technology Overview (only about RAG)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;1. RAG 작동 과정&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Data Gathering(데이터 수집): 다양한 원천 데이터(PDF, TXT, CSV, 웹 URL 등)를 사용해 LLM에 최신 지식이나 심층 지식 전달&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Data Loading: 수집한 데이터를 시스템으로 불러오는 단계 (데이터 구조 파악, 불필요한 텍스트 제거 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Data Splitting: 데이터를 Chunk 단위의 작은 조각으로 나누는 단계. LLM에 불필요한 정보가 제공되는 것을 최소화. (다만 청크 사이즈가 너무 작으면 핵심 정보 누락, 텍스트 맥락의 모호성 발생)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Data Embedding: 청크 단위로 분할된 텍스트를 숫자 벡터로 전환하는 단계. 의미론적 특성 보존을 위해 원천 데이터세 사용된 언어로 충분히 학습한 임베딩 모델 선정 필요&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Data Storing(데이터 저장): 벡터 데이터베이스에 임베딩된 청크와 메타 정보 저장. (벡터 데이터베이스는 Approximate Nearest Neighbor) 알고리즘을 사용하여 고차원 벡터를 효율적으로 인덱싱 하고 검색)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Retrieval(문서 검색): 사용자의 질문과 관련된 정보 검색 (코사인 유사도, 앙상블 기법 등 적용)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Generation(답변 생성): Few-shot learning이나 기타 특화된 프롬프트 템플릿 등을 사용하면 답변 품질 향상 가능. 프롬프트 엔지니어링을 통해 오픈소스 LLM을 최적화.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. RAG 시스템 구축에 활용되는 오케스트레이션 프레임워크&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(1) LangChain&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;모듈화된 구조와 풍부한 통합 기능 제공으로 복잡한 LLM 애플리케이션 개발에 용이&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다양한 LLM, 임베딩 모델, 벡터 저장소에 높은 호환성 제공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Chain과 Agent라는 개념을 토대로 복잡한 작업을 효율적으로 구현 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(2) LlamaIndex&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;대규모 데이터셋에 효율적인 인덱싱과 쿼리 기능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다양한 데이터 소스에 대한 커넥터 제공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;고급 쿼리 엔진 옵션을 통해 정확하고 신속한 정보 검색 지원&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. Limitation of RAG Systems&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(1) RAG 시스템의 성능이 구성 요소의 개별 성능에 크게 의존&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터 분할 시 청크 사이즈 =&amp;gt; 필요한 정보 누락 가능성 =&amp;gt; retriever와 LLM 등 RAG 시스템 전반에 악영향&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;임베딩 모델의 품질 =&amp;gt; 의미적 유사성 포착에 영향 =&amp;gt; 연관 정보 검색에 영향&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;검색기의 성능 =&amp;gt; LLM에 전달되는 정보의 품질 =&amp;gt; 답변의 정확도&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(2) 중간 정보 소실&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;컨텍스트가 길어지면 LLM 정보처리 작업에 오류 발생 가능 (전체 정보를 균형 있게 처리하지 못하고 시작과 끝부분에 위치한 정보에 집중)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;복잡하고 긴 컨텍스트를 다룰 때는 정보 소시 문제를 완화하기 위한 별도의 알고리즘 적용 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(3) 환각 현상 발생 가능성&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;외부 지식 베이스를 사용한다고 하더라도 검색 정보를 잘못 해석하거나 누락하여 환각 현상 발생 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;LLM의 응답에 대한 추가적인 검증 과정 도입 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;연구 내용&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. Proposed method for RAG System (한계점 극복 및 모델 개선을 위한 제안)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(1) 원본 문서의 레이아웃 분석을 위한 알고리즘 개발&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문서에서 텍스트 추출 시 레이아웃에 대한 고려가 없으면 데이터 무결성 훼손&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;결과적으로는 검색기 성능을 저하시켜 LLM이 부적절한 답변일 생성할 확률을 높인다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문서의 레이아웃을 분석하는 알고리즘을 개발하고, 텍스트 추출에 앞서 적용&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;원본 문서의 의미론적 구조 보존&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RAG 시스템의 정보 검색 및 답변 생성 성능 향상&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(2) ensemble retriever(앙상블 검색기) 구현&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다양한 검색 모델을 결합하여 정보 검색의 정확성과 신뢰성을 향상하는 기법&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;hybrid search (sparse retriever + dense retriever)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;sparse retriever: 키워드 기반 문서 탐색&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점: 임베딩 과정 불필요, 검색 속도 빠름&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단점: 동의어나 유사어 인식에 취약&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;dense retriever: 의미적 유사성을 기반으로 관련 정보 검색&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점: 동의어나 유사어 등을 감안하여 검색 결과 제공 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단점: 고성능 임베딩 모델이 전제되지 않으면 연관 문서 검색 성능 크게 저하 가능. 핵심 키워드가 누락된 청크를 가장 관련성 높은 정보로 선별하는 오류를 일으키기도 함.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;BM25(sparse retriever) + FAISS(dense retriever) =&amp;gt; ensemble retrivever 구현&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(3) Re-ranking 알고리즘 적용&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;검색된 정보의 정확성을 높이고, 일부 주요 정보 누락 가능성 개선 목적&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문서 검색 다녜 이후에 재정렬 알고리즘을 적용하여, 검색된 연관 정보의 순서를 효과적으로 재구성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(4) 다층적 Fact-cheking 메커니즘 도입&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;선행 검증: 최종 결론 도출에 앞서, 앙상블 기법이나 프롬프트 엔지니어링 등을 활용하여 LLM 답변을 내부적으로 검증&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Multi-model Ensemble 기법을 활용하여 여러 모델에서 얻은 출력을 비교&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Self-consistency Check을 통해 동일 모델의 다양한 출력 평가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;후행 검증&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;LLM이 산출한 최종 결론에 대해 인간 전문가가 직접 답변의 정확성 평가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;답변의 사실적 정확성, 맥락적 적절성, 일관성, 윤리적 측면까지 종합적으로 평가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 회사 내규 검토 과정 (6단계)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(1) 레이아웃 분석&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다단구조 여부 파악&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;헤더, 푸터 텍스트 확인 (헤더, 푸터 텍스트를 딕셔너리 형태로 저장)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(2) 데이터 로드&amp;amp;전처리&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;원본 문서의 구조적 특성을 감안하여 텍스트 추출&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;헤더/푸터 딕셔너리를 바탕으로 불필요한 텍스트 삭제&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(3) 데이터 분할&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2단계에 걸쳐 전처리된 텍스트가 여러 개의 작은 청크로 분할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[1차 분할] 조항번호 등을 구분 값으로 하여 비교적 큰 사이즈의 청크 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[2차 분할] 1차청크가 더 작은 단위의 청크로 분할되면서, 연관정보 검색에 최적화된 소형 청크 생성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(4) 임베딩&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;허깅페이스에 공개된 'ko-sroberta' 모델 기반&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2차 분할된 청크를 고차원 숫자 벡터로 변환 (벡터 검색을 위한 데이터로 활용)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(5) 연관정보 검색&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Ensemble retriever 활용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용자 질의와 연관된 정보 검색 -&amp;gt; re-ranking으로 재정렬 -&amp;gt; LLM에 전달&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(6) 답변 생성&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;LLM 프롬프트와 재정렬된 연관 정보 참고하여 최종 답변 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;답변에 참고한 정보를 함께 제공&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; Eun-Sil Choi. (2024). Development of an Automated ESG Document Review System using Ensemble-Based OCR and RAG Technologies. Journal of The Korea Society of Computer and Information, 29(9), 25-37. &lt;/span&gt;&lt;/p&gt;</description>
      <category>학교 공부/졸업 프로젝트 (RAG)</category>
      <author>iinana</author>
      <guid isPermaLink="true">https://programming-diary-ina.tistory.com/124</guid>
      <comments>https://programming-diary-ina.tistory.com/124#entry124comment</comments>
      <pubDate>Wed, 9 Apr 2025 16:02:59 +0900</pubDate>
    </item>
    <item>
      <title>[논문 Review] 복지 정책 정보 제공을 위한 RAG 기반 대화형 시스템 개발</title>
      <link>https://programming-diary-ina.tistory.com/123</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;연구 목적&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. LLM의 한계&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용자가 정확한 용어를 알고 있어야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;검색 결과 중 원하는 정보를 직접 찾아야 함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. RAG 도입의 기대효과&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;정책 정보에 대한 접근성 향상&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자연어 기반 질의응답 시스템을 통해 정확하고 관련성 높은 정보 제공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;복잡한 정책 내용을 이해하기 쉬운 형태로 제공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최신 정보를 실시간으로 반영할 수 있는 시스템 구축&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;연구 내용&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 사용 기술&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(1) RAG(Retriever-Augmented Generation)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;대형 언어 모델의 생성 능력과 외부 지식베이스를 결합하는 기술&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용자 쿼리와 관련된 문서나 정보를 외부 지식베이스에서 검색하는 Retriever, 검색된 정보와 쿼리를 바탕으로 응답을 생성하는 Generator로 나뉜다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점 : 외부 지식 베이스를 활용한다&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최신 정보를 반영할 때, 모델을 재학습할 필요가 없이 외부 데이터베이스만 교체하면 된다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존에 모델이 알고 있던 정보로만 답변을 생성하는 것보다 더 정확한 응답 생성이 가능하다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;읽어온 정보의 출처를 알 수 있어 답변의 근거를 제시할 수 있다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특정 분야에 대한 지식베이스를 활용하여 전문성 있는 응답이 가능하다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(2) Prompt Engineering&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 파이프라인&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;FAISS(Faceobok AI Similarity Search) 기반 벡터 검색과 BM25(Best Matching 25)검색을 결합한 앙상블 검색 시스템 구축&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Cross Encoder Reranker를 적용하여 검색 결과 품질 향상&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;GPT를 활용하여 자연스러운 응답 생성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 시스템 설계&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;채팅 UI: HTML, CSS, JavaScript 이용해 설계&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;언어모델이 동작하는 백엔드: FastAPI&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RAG 파이프라인 구축을 위해 LangChain 라이브러리 사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;시스템 구조 도식화&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;803&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tLCHM/btsNc578H2e/LZ5PMD8DBUvCDgYOXsY0kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tLCHM/btsNc578H2e/LZ5PMD8DBUvCDgYOXsY0kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tLCHM/btsNc578H2e/LZ5PMD8DBUvCDgYOXsY0kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtLCHM%2FbtsNc578H2e%2FLZ5PMD8DBUvCDgYOXsY0kK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;438&quot; height=&quot;303&quot; data-origin-width=&quot;803&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. 데이터 수집 및 문서 임베딩&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;PDF 형태로 정책 자료 수집&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;PDF 파싱을 위해 파이썬 PyMuPD 라이브러라 사용&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 외 라이브러리: PDFMiner, PyMuPDF, PDFPlumber, PyPDFium2 등&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;본 연구에서 PyMuPD 선정 이유&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;텍스트 추출의 정확성이 높음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;표와 이미지가 포함된 복잡한 레이아웃도 안정적으로 처리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;유니코드 문자를 올바르게 처리하여 다국어 문서 지원 우수&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;임베딩을 위해 OpenAI의 임베딜 모델 사용&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존 PDF 문서를 800자 단위로 분리하고 100자 단위로 중첩되도록 설정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;임베딩 모델을 통해 벡터 형태로 변환하여 데이터 베이스에 저장&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터베이스로 FAISS 사용&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;일반적으로 사용되는 벡터 데이터베이스는 FAISS, Chroma, Pinecone 등이 있다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;FAISS 선정 이유&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;오픈 소스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;고차원 벡터의 빠른 유사도 검색 지원&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;대량의 데이터처리 시에 뛰어난 성능을 보임&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. 질의 처리 및 검색&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;질의의 의미적 특성을 벡터공간에 매핑하여 문서 벡터들과의 유사도 비교&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Ensemble Retriever(FAISS와 BM25 결합) 기법 사용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;검색 결과 품질 향상을 위해 Cross Encoder Reranker 적용&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;질의와 문서를 동시에 인코딩하여 더 정확한 관련성 점수 계산&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;BAAI/bge-reranker-v2-m3 모델 사용하여 재순위화 수행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;초기 검색 결과 중 상위 5개의 가장 높은 관련성을 가진 문서만 선발하여 최종 답변 생성에 활동&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;6. GPT-4o-mini를 이용한 RAG 구현&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;오픈소스 LLM 대신 GPT를 채택한 이유&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;오픈소스 LLM 구동을 위한 높은 하드웨어 비용 (고성능 GPU 필요)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;모델 운영을 위한 유지보수 비용이 지속적으로 발생한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;내부 데이터 유출 가능성이 존재하나 본 연구에서 사용되는 문서는 공개된 정보이기 때문에 문제 없음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RAG 파이프라인에서 검색된 관련 정보를 바탕으로 답변을 생성하는 역할 수행&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;프롬프트 엔지니어링을 통해 모델이 정책 정보를 정확히 이해하고 사용 친화적인 답변을 생성하도록 유도&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최대한 정확한 정보 전달을 위해 모델의 창의성을 조정하는 temperature 변수 0.1로 설정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;7. LangChain에서 제공하는 모니터링 도구인 LangSmith를 활용하여 시스템 모니터링 및 비용관리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RAG 시스템의 전반적인 성능과 비용 추적&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;검색과 응답 생성에 소요되는 시간 모니터링하여 시스템 성능 최적화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;시스템 운영 중 발생하는 다양한 에러들 추적, 기록&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Ensemble Retriever와 Cross Encoder Reranker 성능을 지속적으로 모니터링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;디버깅 기능으로 RAG의 파이프라인의 각 단계별 처리 과정 상세히 분석 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;전동빈, 김희철 and 김경이. (2025). 복지 정책 정보 제공을 위한 RAG 기반 대화형 시스템 개발. 한국정보통신학회논문지, 29(2), 209-217.&lt;/span&gt;&lt;/p&gt;</description>
      <category>학교 공부/졸업 프로젝트 (RAG)</category>
      <author>iinana</author>
      <guid isPermaLink="true">https://programming-diary-ina.tistory.com/123</guid>
      <comments>https://programming-diary-ina.tistory.com/123#entry123comment</comments>
      <pubDate>Wed, 9 Apr 2025 11:09:26 +0900</pubDate>
    </item>
    <item>
      <title>[운영체제 실습] Scheduling Practice</title>
      <link>https://programming-diary-ina.tistory.com/121</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;운영체제 project를 시작하기 전, 교안에 있는 실습 자료로 sceduling 내용에 관한 practice를 진행했다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ffffff; background-color: #1a5490; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&amp;nbsp;practice 01&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;In xv6, yield is implemented but it is not the system call.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;First, implement yield system call so that user can call yield. (yield gives up its CPU)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Make a user program in xv6 that created child process with fork(), and show that parent and child process print &quot;Parent&quot; and &quot;Child&quot; respectively in loop.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;문제에서 언급된 대로, yield 함수는 이미 구현이 되어있기 때문에 따로 구현하지 않아도 된다. 구현되어 있는 이 함수를 system call에 추가해주기만 하면 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743740861025&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** proc.c **/
// Give up the CPU for one scheduling round.
void
yield(void)
{
  struct proc *p = myproc();
  acquire(&amp;amp;p-&amp;gt;lock);
  p-&amp;gt;state = RUNNABLE;
  sched();
  release(&amp;amp;p-&amp;gt;lock);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. wrapper function 구현&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;우선 구현되어 있는 yield 함수의 wrapper function을 만들어준다. wrapper function이기 때문에 추가적인 기능 없이 그냥 yield 함수를 호출해 주면 된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;다만, 주의할 점은 return type은 꼭 uint64로 해야 한다는 점이다. yield 함수는 return type이 void이다. 즉, 아무것도 리턴할 필요가 없다. 하지만 xv6의 system call table에 등록되는 모든 함수는 반드시 uint64를 반환하도록 정해져 있다. 따라서 나는 그냥 0을 리턴해주고 return type을 uint64로 설정해 주었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743743535314&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** [add code] kernel/sysproc.c **/
uint64
sys_yield(void)
{
  yield();
  return 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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 새로운 system call인 yield를 register&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743743605313&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** [add code] kernel/syscall.h **/
#define SYS_yield  22&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_1743743710690&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** [add code] kernel/syscall.c **/
extern uint64 sys_yield(void); //add

static uint64 (*syscalls[])(void) = {
//생략
[SYS_yield]   sys_yield, //add
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. user.h에 함수 선언&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743743882878&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** [add code] user/user.h **/
void yield(void);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. usys.pl에 macro 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743743936022&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** [add code] user/usys.pl **/
entry(&quot;yield&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. user directory에 user function 구현&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;마지막으로 user function을 만들어줘서, system call로 등록된 yield가 잘 동작하는지 확인해 준다. fork()로 자식 프로세스를 만들어 서로에게 cpu를 넘겨주며 실행하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;다만 여기서 주의할 점은 확인을 위해 &quot;Parent&quot;와 &quot;Child&quot;를 콘솔에 출력할 때, printf가 아닌 write를 써야 한다는 점이다. printf는 비동기적으로 실행되기 때문에 출력이 섞이거나 깨지는 현상 발생할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743744042754&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** [new file] user/yield.c **/
#include &quot;kernel/types.h&quot;
#include &quot;kernel/stat.h&quot;
#include &quot;user/user.h&quot;

void print(const char *msg) {
    write(1, msg, strlen(msg));  // write()를 사용하여 출력
}

int main(int argc, char *argv[])
{
    int pid = fork();
    if (pid &amp;lt; 0) {
        print(&quot;Fork failed!\n&quot;);
        exit(1);
    } else if (pid == 0) {
        for (int i = 0; i &amp;lt; 10; i++) {
            print(&quot;Child\n&quot;);
            yield();
        }
    } else {
        for (int i = 0; i &amp;lt; 10; i++) {
            print(&quot;Parent\n&quot;);
            yield();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;7. user function의 code 파일 Makefile에 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743746737603&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** Makefile **/
UPROGS=\
  // 생략
  $U/_yield\&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;8. 결과: Children과 Parent가 번갈아가면서 실행됨.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ffffff; background-color: #1a5490; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&amp;nbsp;practice 02&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;One basic way to debug is to simpl print the variables, and check if they have intended values.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;Print current value of &lt;i&gt;ticks&lt;/i&gt;&amp;nbsp; variable, and the pid and the name of the process to the console, whenever the sceduler picks a process to run. (Use printf function)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;Make a user program that runs an infinite loop and execute it. (The same process should be picked on ever timer interrupt)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. scheduler function에 현재 상태 print하는 코드 추가&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743746939927&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** [add code] kernel/proc.c **/
void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();

  c-&amp;gt;proc = 0;
  for(;;){
    // 생략
    int found = 0;
    for(p = proc; p &amp;lt; &amp;amp;proc[NPROC]; p++) {
      acquire(&amp;amp;p-&amp;gt;lock);
      if(p-&amp;gt;state == RUNNABLE) {
        printf(&quot;ticks = %u, pid = %u, name = %s\n&quot;, ticks, p-&amp;gt;pid, p-&amp;gt;name); //add
        // 생략
    }
    // 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. test를 위해 user function 만들기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;현재 실행 중인 process의 상태를 print하는 것이므로, 그냥 while문을 통해 해당 프로세스가 무한히 돌아가도록 해주면 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743747002580&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** [new file] ueser/test_print.c **/
#include &quot;kernel/types.h&quot;
#include &quot;kernel/stat.h&quot;
#include &quot;user/user.h&quot;

int main(int argc, char *argv[])
{
    while(1) {}
    return 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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. makefile에 user function code 파일 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743747095589&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** Makefile **/
UPROGS=\
  // 생략
  $U/_test_printf\&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. 결과&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;test_printf 프로세스가 사용자에 의해 terminate 될 때까지 무한히 돌아가는 것을 볼 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSZeoq/btsM83vHeLs/bI2W9xW2lskaaKtjBo1LD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSZeoq/btsM83vHeLs/bI2W9xW2lskaaKtjBo1LD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSZeoq/btsM83vHeLs/bI2W9xW2lskaaKtjBo1LD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSZeoq%2FbtsM83vHeLs%2FbI2W9xW2lskaaKtjBo1LD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;503&quot; height=&quot;430&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>학교 공부/운영체제</category>
      <author>iinana</author>
      <guid isPermaLink="true">https://programming-diary-ina.tistory.com/121</guid>
      <comments>https://programming-diary-ina.tistory.com/121#entry121comment</comments>
      <pubDate>Fri, 4 Apr 2025 15:16:49 +0900</pubDate>
    </item>
    <item>
      <title>[SpringBoot/Java] JavaMailSender로 인증 메일 전송하기</title>
      <link>https://programming-diary-ina.tistory.com/120</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;회원가입 및 로그인 기능을 구현하면서, 인증코드를 담은 메일을 보내는 기능을 구현해야 했다. JavaMailSender를 이용하면 어렵지 않게 구현할 수 있다. 크게 아래 두 가지 기능을 구현해 보았다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 사용자의 이메일로 6자리의 인증코드를 담은 메일 전송&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp; - 인증 코드 생성&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp; - 인증 코드 저장&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp; - 메일 전송&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 사용자가 입력한 코드가 저장된 인증코드와 일치하는지 확인&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;구현은 아래와 같이 했다. 우선 build.gradle 파일에 JavaMailSener를 사용하기 위한 의존성을 추가해 준다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743721401341&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** build.gradle */
dependencies {
    // java mail sender 의존성 추가
    implementation 'org.springframework.boot:spring-boot-starter-mail' 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;그리고 application.yml 파일에 메일 전송 관련 설정을 작성해 준다. host는 보내려고 하는 메일의 도메인에 따라 작성하면 된다. 나는 이 코드를 작성할 때 속해있던 랩실 이메일이 naver 메일이었기 때문에 naver를 선택했다. username에는 보내는 사람, 즉 내 이메일을 작성하면 되고, password에는 해당 이메일에 설정된 비밀번호를 작성하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;port의 경우 사용 가능한 몇가지 옵션이 있지만, 최근 587이 가장 많이 사용되고 추천된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743721462439&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** application.yml */
spring:
  mail:
    host: smtp.naver.com
    port: 587
    username: {sender_email}
    password: {sender_password}
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그리고 인증 코드를 저장할 repository 만들기 위해 RedisConfig를 먼주 구축해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1744013558023&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.survey.server.config;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;

@Configuration
public class RedisConfig {
    // Redis 서버 연결을 위한 ConnectionFactory Bean
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(&quot;localhost&quot;, 6379);
    }

    // LocalDateTime을 위한 커스텀 TypeAdapter
    private static class LocalDateTimeAdapter extends TypeAdapter&amp;lt;LocalDateTime&amp;gt; {
        @Override
        public void write(JsonWriter out, LocalDateTime value) throws IOException {
            if (value == null) {
                out.nullValue();
            } else {
                out.value(value.toString());
            }
        }

        @Override
        public LocalDateTime read(JsonReader in) throws IOException {
            String value = in.nextString();
            return value == null ? null : LocalDateTime.parse(value);
        }
    }

    // RedisSerializer Bean
    @Bean
    public RedisSerializer&amp;lt;Object&amp;gt; gsonRedisSerializer() {
        return new RedisSerializer&amp;lt;&amp;gt;() {
            private final Gson gson = new GsonBuilder()
                    .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
                    .create();

            @Override
            public byte[] serialize(Object source) {
                if (source == null) {
                    return new byte[0];
                }
                return gson.toJson(source).getBytes(StandardCharsets.UTF_8);
            }

            @Override
            public Object deserialize(byte[] bytes) {
                if (bytes == null || bytes.length == 0) {
                    return null;
                }
                String json = new String(bytes, StandardCharsets.UTF_8);
                return gson.fromJson(json, Object.class);
            }
        };
    }

    // RedisTemplate Bean
    @Bean
    public RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate&amp;lt;String, String&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(connectionFactory);

        StringRedisSerializer serializer = new StringRedisSerializer();
        template.setKeySerializer(serializer);
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(serializer);
        template.setHashValueSerializer(serializer);

        return template;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;이제 인증코드를 저장해 둘 repository를 작성한다. 만료시간이 있는 인증코드의 특성상, 편리하게 사용할 수 있는 RedisTemplate으로 설정했다. 유효시간(TTL)은 5분으로 설정했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743721632362&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** VerificationRepository.java */
package com.survey.server.repository;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Repository
@RequiredArgsConstructor
public class VerificationRepository {
    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;
    private static final Long TTL_MINUTES = 5L; // 인증번호 유효시간 5분
    private static final String KEY_PREFIX = &quot;verification: &quot;;

    /**
     * 인증 코드 저장
     * - 이메일, 전송된 랜덤 인증코드, TTL 설정
     */
    public void saveVerifyCode(String userEmail, String verifyCode) {
        redisTemplate.opsForValue()
                .set(getVerificationKey(userEmail), verifyCode,
                        TTL_MINUTES * 60, TimeUnit.SECONDS);
    }

    public Optional&amp;lt;Object&amp;gt; getVerifyCode(String userEmail) {
        Object code = redisTemplate.opsForValue()
                .get(getVerificationKey(userEmail));
        return Optional.ofNullable(code);
    }

    public void deleteVerifyCode(String userEmail) {
        redisTemplate.delete(getVerificationKey(userEmail));
    }

    public String getVerificationKey(String userEmail) {
        return KEY_PREFIX + userEmail;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;마지막으로 아래는 이메일을 전송하고, 인증코드를 확인하는 MailService이다. 내가 짠 프로그램에서는 UserService를 거쳐서 사용되기 때문에 예외처리는 주로 UserService에서 해주었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;내용 자체는 어렵지 않다. @Value 어노테이션으로 application.yml 파일에서 작성했던 username을 보내는 사람의 이메일로 불러온다. 그리고 6자리 코드를 생성해서 앞서 생성한 레포지토리에 저장하고 이를 바탕으로 메일을 전송해 주면 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;인증 번호를 확인할 때는 레포지토리에서 메일 주소를 key로 하여 저장된 인증 번호를 가져와주고, 사용자의 입력값과 일치하는지 확인해 준다. 일치하면 레포지토리에서 해당 코드를 삭제해 주며 true를 리턴하고 일치하지 않으면 false를 리턴한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743722372681&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** MailService.java */
package com.survey.server.service;

import com.survey.server.dto.CodeVerifyDTO;
import com.survey.server.dto.VerificationEmailSendDTO;
import com.survey.server.repository.VerificationRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;

import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

import java.util.NoSuchElementException;

@Service
@RequiredArgsConstructor
public class MailService {
    private final VerificationRepository verificationRepository;
    private final JavaMailSender javaMailSender;

    @Value(&quot;${spring.mail.username}&quot;)
    private String sender;

    public void sendVerificationEmail(String email) {
        String code = String.valueOf(Math.random() * 1000000);
        verificationRepository.saveVerifyCode(email, code);

        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(sender);
        message.setSubject(&quot;[서비스 이름] 이메일 인증 코드&quot;);
        message.setTo(email);
        message.setText(&quot;[인증코드] &quot; + code);

        javaMailSender.send(message);
    }

    /**
     * 인증 코드 확인
     * @param email 확인 요청자 이메일
     * @param inputCode 사용자가 입력한 코드
     * @return 사용자 입력 코드가 저장된 인증 코드와 일치하면 true
     */
    public Boolean verifycode(String email, String inputCode) {
        String code = verificationRepository.getVerifyCode(email)
                .map(Object::toString)
                .orElseThrow(() -&amp;gt; new NoSuchElementException(&quot;Verification code not found&quot;));
        if (code.equals(inputCode)) {
            verificationRepository.deleteVerifyCode(email);
            return true;
        }
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>웹 백엔드 개발/Spring Boot</category>
      <author>iinana</author>
      <guid isPermaLink="true">https://programming-diary-ina.tistory.com/120</guid>
      <comments>https://programming-diary-ina.tistory.com/120#entry120comment</comments>
      <pubDate>Fri, 4 Apr 2025 08:34:38 +0900</pubDate>
    </item>
    <item>
      <title>[SpringBoot/오류 해결] Maria DB에 table이 추가되지 않는 문제</title>
      <link>https://programming-diary-ina.tistory.com/119</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Maria DB를 활용하여 User 관련 기능들을 개발하던 중, user table이 DB에 추가되지 않는 문제가 발생했다. 이상한 점은 refresh token table은 잘 추가가 되었는데, User table만 추가되지 않았다는 점이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아래 코드는 각각 refresh token과 user에 관한 코드이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743635353613&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** RefreshToken.java **/
@NoArgsConstructor
@Getter
@Entity
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name=&quot;id&quot;, updatable=false)
    private Long id;

    @Column(name=&quot;user_id&quot;, nullable=false, unique=true)
    private Long userId;

    @Column(name=&quot;refresh_token&quot;, nullable=false)
    private String refreshToken;

    /** 생략 */
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1743635278742&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** User.java **/
@Entity
@Table(name=&quot;users&quot;)
@Getter
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name=&quot;user_id&quot;, updatable=false)
    private UUID userId;

    @Column(name=&quot;email&quot;, nullable=false, unique=true)
    private String email;

    @Column(name=&quot;user_ident&quot;, nullable=false, unique=true)
    private String userIdent;

    @Column(name=&quot;password&quot;, nullable=false)
    private String password;

    /** 생략 */
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;여러 차이점을 후보로 두고 생각했지만, 결국 문제는 id field의 type이었다. MariaDB에서 UUID 타입을 @GeneratedValue(strategy = GenerationType.IDENTITY)와 함께 사용하는 경우 문제가 될 수 있다고 한다. 따라서 User entity에서도 id의 type을 모두 Long으로 수정해 주었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743635492013&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** User.java **/
@Entity
@Table(name=&quot;users&quot;)
@Getter
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name=&quot;user_id&quot;, updatable=false)
    private Long userId;

    @Column(name=&quot;email&quot;, nullable=false, unique=true)
    private String email;

    @Column(name=&quot;user_ident&quot;, nullable=false, unique=true)
    private String userIdent;

    @Column(name=&quot;password&quot;, nullable=false)
    private String password;

    /** 생략 */
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>웹 백엔드 개발/Spring Boot</category>
      <author>iinana</author>
      <guid isPermaLink="true">https://programming-diary-ina.tistory.com/119</guid>
      <comments>https://programming-diary-ina.tistory.com/119#entry119comment</comments>
      <pubDate>Thu, 3 Apr 2025 08:11:56 +0900</pubDate>
    </item>
  </channel>
</rss>