<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jjung.log</title>
        <link>https://velog.io/</link>
        <description>느려도 천천히라도 기록하는 백엔드 개발자👩🏻‍💻</description>
        <lastBuildDate>Sat, 17 Jan 2026 12:01:28 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jjung.log</title>
            <url>https://velog.velcdn.com/images/study_panda98/profile/de69a584-154b-451a-abfa-fa6ee97b08a6/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jjung.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/study_panda98" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Docker] Docker & CI/CD 자동 배포 구축 매뉴얼]]></title>
            <link>https://velog.io/@study_panda98/Docker-Docker-CICD-%EC%9E%90%EB%8F%99-%EB%B0%B0%ED%8F%AC-%EA%B5%AC%EC%B6%95-%EB%A7%A4%EB%89%B4%EC%96%BC</link>
            <guid>https://velog.io/@study_panda98/Docker-Docker-CICD-%EC%9E%90%EB%8F%99-%EB%B0%B0%ED%8F%AC-%EA%B5%AC%EC%B6%95-%EB%A7%A4%EB%89%B4%EC%96%BC</guid>
            <pubDate>Sat, 17 Jan 2026 12:01:28 GMT</pubDate>
            <description><![CDATA[<h2 id="1-사전-준비-prerequisites">1. 사전 준비 (Prerequisites)</h2>
<p>가장 먼저 준비되어야 할 계정과 서버입니다.</p>
<ul>
<li><strong>AWS EC2 인스턴스(Ubunutu)</strong>: 서버가 생성되어 있고 SSH 접속이 가능해야 함. (인바운드 룰에 22 포트 등록해야함)</li>
<li><strong>Docker Hub 계정</strong>: 이미지를 저장할 저장소(Access Token 발급 필수)</li>
<li><strong>Github Repository</strong>: 프로젝트 소스 코드가 올라갈 곳.</li>
</ul>
<hr>
<h2 id="2-서버ec2-환경-설정-최초-1회">2. 서버(EC2) 환경 설정 (최초 1회)</h2>
<p>EC2 서버에 접속하여 배포에 필요한 환경을 만듭니다.</p>
<h3 id="2-1-docker-설치-및-권한-설정">2-1. Docker 설치 및 권한 설정</h3>
<pre><code class="language-bash"># Install Docker
sudo apt update
sudo apt install docker.io -y

# ubuntu 유저에게 Docker 실행 권한 부여 (sudo 없이 쓰기 위해)
sudo usermod -aG docker ubuntu

# 설정 적용을 위해 쉘 로그아웃 하고 다시 SSH 접속
exit</code></pre>
<h3 id="2-2-디렉토리-및-설정-파일-준비">2-2. 디렉토리 및 설정 파일 준비</h3>
<p>Docker 컨테이너가 읽어야 할 보안 설정파일(properties or yml)은 서버에 직접 심어둡니다.
(보안상의 이유로 운영 설정파일은 github에 올리지 않기 때문에 직접 설정합니다.)</p>
<pre><code class="language-bash"># 프로젝트 폴더 생성
mkdir -p ~/app/&lt;project-name&gt;

# 설정 파일 생성 및 내용 붙여넣기 (DB 비밀번호, JWT secret 등)
vi ~/app/&lt;project-name&gt;/src/main/resources/application-prod.properties
</code></pre>
<hr>
<h2 id="3-github-secrets-설정">3. GitHub Secrets 설정</h2>
<p>Github 저장소의 <code>Settings &gt; Secrets and variables &gt; Actions</code>에 아래 변수들을 등록합니다.</p>
<table>
<thead>
<tr>
<th>Secret 이름</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>DOCKER_USERNAME</td>
<td>Docker Hub 아이디(이메일 아님)</td>
</tr>
<tr>
<td>DOCKER_PASSWORD</td>
<td>Docker Hub Access Token(비밀번호 대신 권장)</td>
</tr>
<tr>
<td>EC2_HOST</td>
<td>EC2 Public IP 주소(또는 도메인)</td>
</tr>
<tr>
<td>EC2_USERNAME</td>
<td>ubuntu</td>
</tr>
<tr>
<td>EC2_SSH_KEY</td>
<td>EC2접속용 <code>.pem</code> 키 내용 전체(<code>-----BEGIN-----</code>포함)</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-프로젝트-파일-생성local">4. 프로젝트 파일 생성(local)</h2>
<p>이제 프로젝트(IntelliJ 등)에서 3개의 파일을 만들거나 수정합니다.</p>
<h3 id="4-1-dockerfile-프로젝트-최상위-경로">4-1. Dockerfile (프로젝트 최상위 경로)</h3>
<p>서버 환경을 이미지로 정의하는 파일입니다.</p>
<pre><code class="language-bash">FROM eclipse-temurin:17-jdk

ARG JAR_FILE=build/libs/life-manager-1.0.0.jar

COPY ${JAR_FILE} app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-Duser.timezone=Asia/Seoul&quot;, &quot;-Dspring.profiles.active=prod&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]
</code></pre>
<h3 id="4-2-deploysh-프로젝트-최상위-경로">4-2. deploy.sh (프로젝트 최상위 경로)</h3>
<p>서버에서 실제로 컨테이너를 갈아끼우는 스크립트입니다.</p>
<pre><code class="language-bash">#!/bin/bash

# Docker 이미지 이름(본인 ID로 수정 필수)
DOCKER_APP_NAME=&quot;&lt;your-docker-id&gt;/&lt;project-name&gt;&quot;

echo &quot;===== Docker 배포 시작 =====&quot;

# 기존 컨테이너가 있다면 중지 및 삭제
if [&quot;$(docker ps -a -q -f name=&lt;container-name&gt;)&quot;]; then
   echo &quot;Stopping and removing existing container...&quot;
   docker stop &lt;container-name&gt;
   docker rm &lt;container-name&gt;
fi

# 기존 이미지 삭제 (용량 확보, 에러 무시)
echo &quot;Removing old image...&quot;
docker rmi $DOCKER_APP_NAME:latest 2&gt;/dev/null || true

# 새 이미지 다운로드
echo &quot;Pulling new image...&quot;
docker pull $DOCKER_APP_NAME:latest

# 새 컨테이너 실행(Volumne mount 포함)
echo &quot;Running container...&quot;
docker run -d -p 9000:9000 \
--name &lt;container-name&gt; \
-v /home/ubuntu/app/&lt;project-name&gt;/src/main/resources/application-prod.properties:/config/application-prod.properties \
$DOCKER_APP_NAME:latest

# 불필요한 이미지 정리
docker image prune -f

echo &quot;===== Docker 배포 완료 =====&quot;
</code></pre>
<h3 id="4-3-githubworkflowsdeployyml">4-3. <code>.github/workflows/deploy.yml</code></h3>
<p>GitHub Actions가 수행할 작업 지시입니다.</p>
<pre><code class="language-yaml">name: Deploy to EC2 with Docker

&#39;on&#39;:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: &quot;17&quot;
          distribution: &quot;temurin&quot;

      - name: Build with Gradle
        run: |
          chmod +x gradlew
          ./gradlew clean build -x test

      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/&lt;project-name&gt;:latest

      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          scripts: |
            cd /home/ubuntu/app/&lt;project-name&gt;
            git pull origin main

            chmod +x deploy.sh
            ./deploy.sh</code></pre>
<hr>
<h2 id="5-배포-실행-execution">5. 배포 실행 (Execution)</h2>
<p>모든 준비가 끝났습니다.</p>
<ol>
<li>로컬에서 코드를 수정합니다.</li>
<li><code>git add</code>, <code>git commit</code>, <code>git push origin main</code>을 입력합니다.</li>
<li>GitHub Actions 탭에서 초록색 체크를 확인합니다.</li>
<li>서버가 자동으로 업데이트 됩니다.</li>
</ol>
<hr>
<h2 id="💡-핵심-포인트-troubleshooting">💡 핵심 포인트 (Troubleshooting)</h2>
<ul>
<li><strong>설정 파일 연결</strong>: Docker는 격리된 환경이므로, EC2에 있는 <code>application-prod.properties</code>를 <code>-v</code>옵션으로 반드시 연결(마운트)해줘야 합니다.</li>
<li><strong>스크립트 갱신</strong>: <code>deploy.yml</code>의 script 부분에 <code>git pull</code>이 있어야, <code>deploy.sh</code>의 변경사항(포트 변경 등)이 서버에 반영됩니다.</li>
<li><strong>이미지 이름</strong>: <code>deploy.sh</code>와 <code>deploy.yml</code>에 적힌 이미지 태그(<code>ID/repo:latest</code>)가 정확히 일치해야 합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nginx] EC2에서 Nginx, HTTPS 설정하기]]></title>
            <link>https://velog.io/@study_panda98/DevOps-EC2%EC%97%90%EC%84%9C-Nginx-HTTPS-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@study_panda98/DevOps-EC2%EC%97%90%EC%84%9C-Nginx-HTTPS-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 16 Jan 2026 15:18:19 GMT</pubDate>
            <description><![CDATA[<h2 id="reverse-proxy">Reverse Proxy</h2>
<blockquote>
<p>클라이언트와 백엔드 서버 사이에 위치하여 클라이언트의 요청을 대신 받아 백엔드 서버로 전달하고 응답을 다시 클라이언트에게 돌려주는 중간 서버 역할</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/study_panda98/post/3a7fe77e-1950-4c9f-ba65-2f66329f28f7/image.jpg" alt="General Server vs Server with Nginx"></p>
<h3 id="리버스-프록시의-장점">리버스 프록시의 장점</h3>
<ol>
<li>보안 강화: 백엔드 서버의 실제 IP와 포트를 외부에 노출 시키지 않음.</li>
<li>SSL/TLS 종료: HTTPS 암호화/복호화를 프록시에서 처리하여 백엔드 서버 부담 감소</li>
<li>로드 밸런싱: 여러 백엔드 서버로 트래픽 분산 가능</li>
<li>캐싱: 정적 컨텐츠 캐싱으로 성능 향상</li>
<li>단일 진입점: 여러 서비스를 하나의 도메인으로 통합 (CORS 해결)</li>
</ol>
<hr>
<h2 id="ec2에-https-설정하기">EC2에 HTTPS 설정하기</h2>
<h3 id="사전-준비-사항">사전 준비 사항</h3>
<ul>
<li>EC2 Instance (Ubuntu)</li>
<li>Domain Address</li>
<li>도메인의 DNS A 레코드가 EC2의 Public IP를 가리키도록 설정<blockquote>
<p>DNS A 레코드 : 도메인 &lt;-&gt; IPv4 주소로 매칭해주는 가장 기본적인 DNS 레코드</p>
</blockquote>
</li>
<li>EC2 보안 그룹에서 80, 443 포트 인바운드 허용</li>
</ul>
<h3 id="1-nginx-설치">1. NGINX 설치</h3>
<pre><code class="language-bash">sudo apt update
sudo apt install nginx -y
sudo systemctl start nginx

# 부팅 시, 자동 시작 설정
sudo systemctl enable nginx
sudo systemctl status nginx</code></pre>
<h3 id="2-nginx-기본-설정http">2. NGINX 기본 설정(HTTP)</h3>
<p>먼저 HTTP로 리버스 프록시를 설정하고 테스트합니다.</p>
<pre><code class="language-bash">sudo vi /etc/nginx/sites-available/&lt;project-name&gt;

# 설정파일 내용
server {
    listen 80;
    server_name &lt;domain-address&gt; # 실제 도메인

    location / {
        proxy_pass http://localhost:8080; # Spring Boot 실행 포트 입력하기
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwared-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwared-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

# 심볼릭 링크 생성하여 설정 활성화
sudo ln -s /etc/nginx/sites-available/&lt;project-name&gt; /etc/nginx/sites-enabled/

# 기본 설정 비활성화 (선택사항)
sudo rm /etc/nginx/sites-enabled/default

# 설정파일 문법 검사
sudo nginx -t

# NGINX 재시작
sudo systemctl restart nginx</code></pre>
<h3 id="3-https-설정-certbot-사용">3. HTTPS 설정 (Certbot 사용)</h3>
<h4 id="certbot-설치">Certbot 설치</h4>
<p>Certbot을 이용하여 SSL 인증서를 발급 받고 HTTPS를 설정해보겠습니다.</p>
<pre><code class="language-bash"># certbot 설치
sudo apt install certbot python3-certbot-nginx -y

# SSL 인증서 발급 및 자동 설정
sudo certbot --nginx -d &lt;domain-address&gt;</code></pre>
<p>실행 시, 다음 정보를 입력하게 됩니다.</p>
<p>1) 이메일 주소: 인증서 만료 알림 수신용
2) 서비스 약관 동의: Y 입력
3) HTTP -&gt; HTTPS 리다이렉트 설정: 2 선택</p>
<h4 id="자동-생성된-nginx-설정-확인">자동 생성된 NGINX 설정 확인</h4>
<p>Certbot이 설정 파일을 자동으로 수정합니다.</p>
<pre><code class="language-bash">sudo cat /etc/nginx/sites-available/&lt;project-name&gt;

# life-manager 설정
server {
    server_name &lt;domain-address&gt;;

    # 루트 경로 - 상태 메세지 반환
    location = / {
        return 200 &#39;{&quot;status&quot;:&quot;UP&quot;,&quot;service&quot;:&quot;Life Manager API&quot;}&#39;;
        add_header Content-Type application/json;
    }

    location /api/ {
        proxy_pass http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection &#39;upgrade&#39;;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/&lt;domain-address&gt;/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/&lt;domain-address&gt;/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

server {
    if ($host = &lt;domain-address&gt;) {
        return 301 https://$host$request_uri;
    }

    listen 80;
    server_name &lt;domain-address&gt;;
    return 404;
}</code></pre>
<h4 id="인증서-자동-갱신-설정">인증서 자동 갱신 설정</h4>
<p>Let&#39;s Encrypt 인증서는 <code>90일마다</code> 만료됩니다. Certbot은 자동으로 갱신 타이머를 설정합니다.</p>
<pre><code class="language-bash"># 자동 갱신 타이머 상태 확인
sudo systemctl status certbot.timer

# 갱신 테스트 (실제 갱신하지 않고 시뮬레이션 하는거임)
sudo certbot renew --dry-run</code></pre>
<h4 id="설정-테스트">설정 테스트</h4>
<pre><code class="language-bash">sudo nginx -t
sudo systemctl restart nginx

curl -I https://&lt;domain-address&gt;</code></pre>
<hr>
<h3 id="번외-알아두면-유용한-명령어-모음">(번외) 알아두면 유용한 명령어 모음</h3>
<p>사실 내가 까먹을거 같아서 적음😂</p>
<pre><code class="language-bash"># nginx 관련
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx

# 로그 확인
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

# 인증서 관련
sudo certbot certificates # 발급된 인증서 목록
sudo certbot renew          # 인증서 갱신
sudo certbot delete          # 인증서 삭제</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 소셜 로그인 구현하기(JWT 사용)]]></title>
            <link>https://velog.io/@study_panda98/OAuth-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0JWT-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@study_panda98/OAuth-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0JWT-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Fri, 09 Jan 2026 02:00:29 GMT</pubDate>
            <description><![CDATA[<p>이번에 개인 프로젝트를 진행하면서 구현했던 OAuth(소셜 로그인) 연동 과정을 기록해보려 합니다.
<code>client-id</code>, <code>client-secret</code> 등을 발급 받는 방법은 구글, 네이버 등 모두 비슷하니 카카오에서 발급하는 방법만 아래에 기술하겠습니다.</p>
<blockquote>
<p>개발 환경 : Spring Boot 3.5, Java 17, JWT 사용</p>
</blockquote>
<h1 id="카카오-간편-로그인-구현">카카오 간편 로그인 구현</h1>
<h2 id="서비스-로그인-과정-sequence-diagram">서비스 로그인 과정 Sequence Diagram</h2>
<p><img src="https://velog.velcdn.com/images/study_panda98/post/4a9d86b5-e9c9-4e0b-bed8-a9e73b6af47e/image.png" alt="출처 : 카카오 디벨로퍼즈 문서"></p>
<h3 id="1-카카오-디벨로퍼httpsdeveloperskakaocom-가입하기">1. 카카오 디벨로퍼(<a href="https://developers.kakao.com/">https://developers.kakao.com/</a>) 가입하기</h3>
<h3 id="2-앱--앱-생성-에서-기본-정보-입력">2. <code>앱 &gt; 앱 생성</code> 에서 기본 정보 입력</h3>
<p><img src="https://velog.velcdn.com/images/study_panda98/post/85a9693a-cef6-4c20-b663-1fcd46dc9e9c/image.png" alt="create-app"></p>
<h3 id="3-앱-설정--앱--생성한-앱-클릭--플랫폼-키에서-개발하는-환경에-맞는-키를-applicationproperties-에-붙여넣기">3. <code>앱 설정 &gt; 앱 &gt; 생성한 앱 클릭 &gt; 플랫폼 키</code>에서 개발하는 환경에 맞는 키를 application.properties 에 붙여넣기</h3>
<p><strong>(GitHub Repository에 노출 안되게 조심하기‼️)</strong></p>
<pre><code># 어떤 앱에서 요청이 왔는지 카카오에서 식별할 식별 키
spring.security.oauth2.client.registration.kakao.client-id=YOUR_KAKAO_REST_API_KEY</code></pre><h3 id="4-rest-api-키-하단에-있는-클라이언트-시크릿--카카오-로그인-코드-복사-및-붙여넣기">4. REST API 키 하단에 있는 <code>클라이언트 시크릿 &gt; 카카오 로그인</code> 코드 복사 및 붙여넣기</h3>
<p><strong>(GitHub Repository에 노출 안되게 조심하기‼️)</strong></p>
<pre><code># API 키와 함께 앱을 인증하는 비밀 키. access_token을 요청할 때 필요함
spring.security.oauth2.client.registration.kakao.client-secret=YOUR_KAKAO_CLIENT_SECRET</code></pre><p><img src="https://velog.velcdn.com/images/study_panda98/post/04478917-90f4-4f97-9c19-bdcb012e1f66/image.png" alt="api-key"></p>
<h3 id="5-카카오-로그인-성공-후-authorization-code를-받을-백엔드-주소-등록">5. 카카오 로그인 성공 후 authorization code를 받을 백엔드 주소 등록</h3>
<ul>
<li>프론트엔드에서 버튼을 눌렀을 때 이동할 주소와 동일해야 오류가 발생하지 않아요</li>
<li><code>REST API 키 수정 화면 &gt; 카카오 로그인 리다이렉트 URI</code> 에 동일하게 등록이 필요해요<pre><code># 카카오 로그인 성공 후 authorization code를 받을 Callback URI(각자 정하기 나름!)
spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/api/login/oauth2/code/kakao</code></pre></li>
</ul>
<h3 id="6-아래-설정을-applicationproperties-에-붙여넣기">6. 아래 설정을 application.properties 에 붙여넣기</h3>
<pre><code># OAuth2 - Kakao
# OAuth의 여러 인증 방식 중 Authorization code grant를 사용하겠다는 설정
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code

# 카카오에서 가져올 사용자의 정보 범위 설정
spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email

# client-secret을 어떻게 전달할지 정하는 설정
spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post

# Provider 정보
# 사용자가 카카오 로그인 페이지로 리다이렉트 되는 URL
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize

# authorization code(인가코드 : code의 값)를 access_token으로 교환할 엔드포인트
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token

# access_token으로 카카오 사용자의 정보를 조회하는 엔드포인트
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me

# 응답에서 사용자의 고유 식별자로 사용할 필드명
spring.security.oauth2.client.provider.kakao.user-name-attribute=id


# OAuth2 - Google
spring.security.oauth2.client.registration.google.client-id=YOUR_GOOGLE_CLIENT_ID
spring.security.oauth2.client.registration.google.client-secret=YOUR_GOOGLE_CLIENT_SECRET
spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/api/login/oauth2/code/google
spring.security.oauth2.client.registration.google.scope=profile,email

# OAuth2 - Naver
spring.security.oauth2.client.registration.naver.client-id=YOUR_NAVER_CLIENT_ID
spring.security.oauth2.client.registration.naver.client-secret=YOUR_NAVER_CLIENT_SECRET
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/api/login/oauth2/code/naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email

spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response</code></pre><hr>
<h1 id="프로젝트-구현">프로젝트 구현</h1>
<p>이제 설정 파일을 바탕으로 실제 자바 코드를 작성해 보겠습니다.</p>
<h2 id="1-의존성-추가-buildgradle">1. 의존성 추가 (build.gradle)</h2>
<p>Spring Security와 OAuth2 Client 라이브러리를 추가합니다.</p>
<pre><code class="language-java">dependencies {
    // Spring Security &amp; OAuth2 Client
    implementation &#39;org.springframework.boot:spring-boot-starter-oauth2-client&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;

    // Web (Controller 등)
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;

    // Lombok, DB 관련 의존성은 프로젝트 환경에 맞게 추가
    compileOnly &#39;org.projectlombok:lombok&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;
}</code></pre>
<h2 id="2-securityconfig-설정">2. SecurityConfig 설정</h2>
<p>스프링 부트 3.x 버전부터는 <code>SecurityFilterChain</code>을 Bean으로 등록하여 보안 설정합니다.
저는 JWT 사용을 위해 세션을 Stateless로 설정하고, <code>application.properties</code>에 지정한 리다이렉트 URI 경로와 일치하도록 엔드포인트를 커스텀합니다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomOAuth2UserService customOAuth2UserService;
    private final OAuth2SuccessHandler oAuth2SuccessHandler;
    private final JwtAuthenticationFilter jwtFilter;

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

        http
            .csrf(csrf -&gt; csrf.disable()) // CSRF 비활성화
            .formLogin(form -&gt; form.disable()) // Form Login 비활성화
            .httpBasic(basic -&gt; basic.disable()) // HTTP Basic 비활성화
            .sessionManagement(session -&gt; 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 미사용 (JWT)
            )
            .authorizeHttpRequests(auth -&gt; auth
                .requestMatchers(&quot;/&quot;, &quot;/api/auth/**&quot;).permitAll()
                .requestMatchers(&quot;/login/oauth2/**&quot;, &quot;/oauth2/**&quot;).permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -&gt; oauth2
                .authorizationEndpoint(endpoint -&gt; endpoint
                    .baseUri(&quot;/api/oauth2/authorization&quot;) // 소셜 로그인 연결 주소 커스텀
                )
                .redirectionEndpoint(endpoint -&gt; endpoint
                    .baseUri(&quot;/api/login/oauth2/code/*&quot;) // 리다이렉트 주소 커스텀
                )
                .userInfoEndpoint(userInfo -&gt; userInfo
                    .userService(customOAuth2UserService) // 사용자 정보 로드 서비스 등록
                )
                .successHandler(oAuth2SuccessHandler) // 로그인 성공 시 JWT 발급 처리
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}
</code></pre>
<h2 id="3-userentity--repository">3. UserEntity &amp; Repository</h2>
<p>소셜 로그인으로 가져온 사용자 정보를 저장할 User Entity와 User Repository입니다.</p>
<pre><code class="language-java">// User Entity
@Entity
@Table(name = &quot;users&quot;)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User extends BaseEntity {

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

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

    private String password; // OAuth 사용자는 null

    @Column(nullable = false, length = 100)
    private String name;

    @Column(length = 500)
    private String profileImageUrl;

    @Column(length = 50)
    private String provider; //  &#39;kakao&#39;, &#39;google&#39;, &#39;naver&#39;

    @Column(length = 255)
    private String providerId; // OAuth 제공자의 사용자 ID

}

// User Repository
import com.lifemanager.life_manager.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

    Optional&lt;User&gt; findByEmail(String email);

    Optional&lt;User&gt; findByProviderAndProviderId(String provider, String providerId);

    boolean existsByEmail(String email);
}

</code></pre>
<h2 id="4-oauth2userinfo-interface--implementations">4. OAuth2UserInfo (Interface &amp; Implementations)</h2>
<p>여러 소셜 로그인(구글, 카카오, 네이버)의 응답 형태가 서로 다르기 때문에, 공통 인터페이스를 정의하고 각 소셜 서비스별로 구현체를 만들어 데이터를 통일성 있게 처리합니다.</p>
<h3 id="1-oauth2userinfo-interface">1) OAuth2UserInfo Interface</h3>
<pre><code class="language-java">public interface OAuth2UserInfo {
    String getProviderId();
    String getProvider();
    String getEmail();
    String getName();
}</code></pre>
<h3 id="2-구현체-kakao-google-naver">2) 구현체 (Kakao, Google, Naver)</h3>
<ul>
<li><strong>KakaoOAuth2UserInfo</strong>
카카오는 <code>kakao_account</code> 내부에 <code>profile</code>이 존재하는 등 구조가 깊으므로 계층적인 파싱이 필요합니다.</li>
</ul>
<pre><code class="language-java">import java.util.Map;

public class KakaoOAuth2UserInfo implements OAuth2UserInfo {

    private Map&lt;String, Object&gt; attributes;

    public KakaoOauth2UserInfo(Map&lt;String, Object&gt; attributes) {
        this.attributes = attributes;
    }

    @Override
    public String getProviderId() {
        return String.valueOf(attributes.get(&quot;id&quot;);
    }

    @Override
    public String getProvider() {
        return &quot;kakao&quot;;
    }

    @Override
    @SuppressWarnings(&quot;unchecked&quot;)
    public String getEmail() {
        Map&lt;String, Object&gt; kakaoAccount = (Map&lt;String, Object&gt;) attributes.get(&quot;kakao_account&quot;);

        if (kakaoAccount == null) {
            return null;
        }

        return (String) kakaoAccount.get(&quot;email&quot;);
    }

    @Override
    @SuppressWarnings(&quot;unchecked&quot;)
    public String getName() {
        Map&lt;String, Object&gt; properties = (Map&lt;String, Object&gt;) attributes.get(&quot;properties&quot;);

        if (properties == null) {
            return null;
        }

        return (String) properties.get(&quot;nickname&quot;);
    }
}</code></pre>
<ul>
<li><strong>GoogleOAuth2UserInfo</strong><pre><code class="language-java">import java.util.Map;
</code></pre>
</li>
</ul>
<p>public class GoogleOAuth2UserInfo implements OAuth2UserInfo {
    private Map&lt;String, Object&gt; attributes;</p>
<pre><code>public GoogleOAuth2UserInfo(Map&lt;String, Object&gt; attributes) {
    this.attributes = attributes;
}

@Override
public String getProviderId() {
    return (String) attributes.get(&quot;sub&quot;);
}

@Override
public String getProvider() {
    return &quot;google&quot;;
}

@Override
public String getEmail() {
    return (String) attributes.get(&quot;email&quot;);
}

@Override
public String getName() {
    return (String) attributes.get(&quot;name&quot;);
}</code></pre><p>}</p>
<pre><code>
- **NaverOAuth2UserInfo**
네이버는 `response`라는 키 값 안에 사용자 정보가 담겨 옵니다.
```java
import java.util.Map;

public class NaverOAuth2UserInfo implements OAuth2UserInfo {
    private Map&lt;String, Object&gt; attributes;

    public NaverOAuth2UserInfo(Map&lt;String, Object&gt; attributes) {
        this.attributes = attributes;
    }

    @Override
    @SuppressWarnings(&quot;unchecked&quot;)
    public String getProviderId() {
        Map&lt;String, Object&gt; response = (Map&lt;String, Object&gt;) attributes.get(&quot;response&quot;);
        if (response == null) {
            return null;
        }
        return (String) response.get(&quot;id&quot;);
    }

    @Override
    public String getProvider() {
        return &quot;naver&quot;;
    }

    @Override
    @SuppressWarnings(&quot;unchecked&quot;)
    public String getEmail() {
        Map&lt;String, Object&gt; response = (Map&lt;String, Object&gt;) attributes.get(&quot;response&quot;);
        if (response == null) {
            return null;
        }
        return (String) response.get(&quot;email&quot;);
    }

    @Override
    @SuppressWarnings(&quot;unchecked&quot;)
    public String getName() {
        Map&lt;String, Object&gt; response = (Map&lt;String, Object&gt;) attributes.get(&quot;response&quot;);
        if (response == null) {
            return null;
        }
        return (String) response.get(&quot;name&quot;);
    }
}</code></pre><h2 id="5-customoauth2user">5. CustomOAuth2User</h2>
<p>Security Context에 저장된 인증 객체입니다. 우리 서비스의 <code>User</code> 엔티티와 OAuth2 제공자가 리턴한 <code>OAuth2User</code> 객체를 함께 보관하여, 컨트롤러 등에서 쉽게 사용자 정보를 꺼내 쓸 수 있도록 래핑(Wrapping) 합니다.</p>
<pre><code class="language-java">import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Collection;
import java.util.Map;

@Getter
public class CustomOAuth2User implements OAuth2User {

    private OAuth2User oAuth2User;
    private User user;

    public CustomOAuth2User(OAuth2User oAuth2User, User user) {
        this.oAuth2User = oAuth2User;
        this.user = user;
    }

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

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

    @Override
    public String getName() {
        return oAuth2User.getName();
    }
}</code></pre>
<h2 id="6-customoauth2userservice">6. CustomOAuth2UserService</h2>
<p>OAuth2 공급자로부터 받은 사용자 정보를 가공하여 회원가입 또는 정보 수정을 처리하고, <code>CustomOAuth2User</code>를 반환합니다.</p>
<pre><code class="language-java">import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.Optional;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // 1. 소셜 로그인 API의 사용자 정보 요청
        OAuth2User oAuth2User = super.loadUser(userRequest);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        Map&lt;String, Object&gt; attributes = oAuth2User.getAttributes();

        // 2. 소셜 타입에 맞게 유저 정보 추출
        OAuth2UserInfo oAuth2UserInfo = getOAuth2UserInfo(registrationId, attributes);

        if (oAuth2UserInfo.getEmail() == null) {
            throw new OAuth2AuthenticationException(&quot;이메일을 가져올 수 없습니다&quot;);
        }

        // 3. 회원가입 또는 정보 업데이트
        User user = saveOrUpdate(oAuth2UserInfo);

        // 4. User 객체를 포함한 CustomOAuth2User 반환
        return new CustomOAuth2User(oAuth2User, user);
    }

    private OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map&lt;String, Object&gt; attributes) {
        return switch (registrationId) {
            case &quot;kakao&quot; -&gt; new KakaoOAuth2UserInfo(attributes);
            case &quot;google&quot; -&gt; new GoogleOAuth2UserInfo(attributes);
            case &quot;naver&quot; -&gt; new NaverOAuth2UserInfo(attributes);
            default -&gt; throw new OAuth2AuthenticationException(&quot;지원하지 않는 소셜 로그인입니다: &quot; + registrationId);
        };
    }

    private User saveOrUpdate(OAuth2UserInfo oAuth2UserInfo) {
        // 소셜 로그인 유저를 구분하기 위해 &#39;provider_email&#39; 형식으로 이메일 저장
        String email = oAuth2UserInfo.getProvider() + &quot;_&quot; + oAuth2UserInfo.getEmail();

        Optional&lt;User&gt; userOptional = userRepository.findByEmail(email);

        User user;
        if (userOptional.isPresent()) {
            user = userOptional.get();
            user.setName(oAuth2UserInfo.getName());
        } else {
            user = User.builder()
                    .email(email)
                    .password(passwordEncoder.encode(UUID.randomUUID().toString()))
                    .name(oAuth2UserInfo.getName())
                    .provider(oAuth2UserInfo.getProvider())
                    .providerId(oAuth2UserInfo.getProviderId())
                    .build();
            log.info(&quot;새로운 OAuth2 사용자 등록 - provider: {}, email: {}&quot;,
                    oAuth2UserInfo.getProvider(), oAuth2UserInfo.getEmail());
        }

        return userRepository.save(user);
    }
}</code></pre>
<h2 id="7-jwt--handler-구현">7. JWT &amp; Handler 구현</h2>
<p><code>SecurityConfig</code>에서 사용되는 JWT 관련 컴포넌트와 로그인 성공 핸들러입니다.</p>
<h3 id="1-jwttokenprovider">1) JwtTokenProvider</h3>
<p>JWT 토큰 생성 및 검증을 담당하는 클래스입니다.</p>
<pre><code class="language-java">@Component
@Slf4j
public class JwtTokenProvider {

    @Value(&quot;${jwt.secret}&quot;)
    private String secretKey;

    @Value(&quot;${jwt.expiration}&quot;)
    private long expirationTime;

    private Key key;

    @PostConstruct
    public void init() {
        this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
    }

    public String generateToken(String email, String role) {
        return Jwts.builder()
                .setSubject(email)
                .claim(&quot;role&quot;, role)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expirationTime))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            log.error(&quot;Invalid JWT token&quot;, e);
            return false;
        }
    }

    // ... 토큰에서 사용자 정보 추출 메서드 등 추가
}</code></pre>
<h3 id="2-oauth2successhandler">2) OAuth2SuccessHandler</h3>
<p>소셜 로그인 성공 후 실행되는 핸들러입니다.
<code>CustomOAuth2UserService</code>에서 넘겨준 인증 정보를 바탕으로 JWT를 생성하고, 프론트엔드로 리다이렉트 시킵니다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();

        String accessToken = jwtTokenProvider.generateToken(oAuth2User.getName(), oAuth2User.getUser().getRole().getKey());

        // 프론트엔드 주소로 리다이렉트 (쿼리 파라미터로 토큰 전달)
        String targetUrl = UriComponentsBuilder.fromUriString(&quot;http://localhost:3000/oauth2/redirect&quot;)
                .queryParam(&quot;accessToken&quot;, accessToken)
                .build().toUriString();

        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}</code></pre>
<h3 id="3-jwtauthenticationfilter">3) JwtAuthenticationFilter</h3>
<p>모든 요청에 대해 헤더를 검사하여 유효한 JWT 토큰이 있는지 확인하는 필터입니다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = resolveToken(request);

        if (token != null &amp;&amp; jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효하면 SecurityContext에 인증 정보 저장
            // ... (Authentication 객체 생성 및 저장 로직)
        }

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(&quot;Authorization&quot;);
        if (bearerToken != null &amp;&amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
            return bearerToken.substring(7);
        }
        return null;
    }
}</code></pre>
<h2 id="8-로그인-테스트">8. 로그인 테스트</h2>
<p>간단한 index.html을 만들어 테스트 해보겠습니다.
Spring Security OAuth2 Client는 기본적으로 <code>/oauth2/authorization/{registrationId}</code> 형식의 요청을 가로채서 소셜 로그인 페이지로 리다이렉트합니다.</p>
<pre><code class="language-html">&lt;!-- index.html --&gt;
&lt;h1&gt;소셜 로그인 테스트&lt;/h1&gt;
&lt;a href=&quot;/api/oauth2/authorization/kakao&quot; class=&quot;btn btn-warning&quot;&gt;Kakao Login&lt;/a&gt;
&lt;a href=&quot;/api/oauth2/authorization/google&quot; class=&quot;btn btn-primary&quot;&gt;Google Login&lt;/a&gt;
&lt;a href=&quot;/api/oauth2/authorization/naver&quot; class=&quot;btn btn-success&quot;&gt;Naver Login&lt;/a&gt;</code></pre>
<hr>
<h2 id="💡-트러블-슈팅-자주-겪는-오류">💡 트러블 슈팅 (자주 겪는 오류)</h2>
<p><strong>Q. redirect_uri_mismatch 에러가 떠요!</strong>
A. 개발자 센터의 <code>Redirect URI</code> 설정과 <code>application.properties</code>의 설정이 토씨 하나 안 틀리고 정확히 일치해야 합니다. 특히 <code>http vs https</code>, 혹은 끝에 <code>/</code> 유무를 확인해 보세요.</p>
<br />

<p><strong>Q. NullPointerException이 발생해요.</strong>
A. CustomOAuth2UserService에서 카카오의 응답 구조(Map)를 파싱할 때 키 값이 잘못되었을 확률이 높습니다. Log4j를 추가하여 <code>log.debug(attributes)</code> 혹은 <code>log.info(attributes)</code>로 OAuth2 제공자(Kakao, Google, Naver)가 보내주는 전체 JSON 데이터를 로그로 찍어서 구조를 확인해보세요.</p>
<h2 id="마치며">마치며</h2>
<p>내용이 정말 길어서 따라오기 벅차긴 했지만, 구현하니 뿌듯하더라구요☺️
여기까지 따라오신 분들 모두 구현에 성공하시길 바랄게요!
질문은 댓글로 남겨주시면 답변 드리겠습니다.</p>
<p>감사합니다🙏</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PostgreSQL 기본 문법]]></title>
            <link>https://velog.io/@study_panda98/PostgreSQL-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</link>
            <guid>https://velog.io/@study_panda98/PostgreSQL-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</guid>
            <pubDate>Tue, 30 Dec 2025 09:39:21 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요!
오늘은 많이 쓰이고 있는 RDBMS인 PostgreSQL의 기본 문법을 소개해드리겠습니다.</p>
<h2 id="데이터베이스-생성-및-유저-생성">데이터베이스 생성 및 유저 생성</h2>
<pre><code class="language-sql">// &lt;YOUR_DATABASE&gt; : 만들고 싶은 데이터베이스명
CREATE DATABASE &lt;YOUR_DATABASE&gt;;

// &lt;USERNAME&gt; : 본인이 만들고 싶은 유저의 이름
// YOUR_PASSWORD : 유저의 비밀번호(꼭 기억하기)
CREATE USER &lt;USERNAME&gt; WITH PASSWORD &#39;YOUR_PASSWORD&#39;;

// USERNAME에게 DATABASE의 모든 권한을 부여한다는 의미
GRANT ALL PRIVILEGES ON DATABASE &lt;YOUR_DATABASE&gt; TO &lt;USERNAME&gt;;

// PostgreSQL 15부터는 생성한 DB로 변경 후 추가로 이것도 필요해요
GRANT ALL ON SCHEMA public TO &lt;USERNAME&gt;;</code></pre>
<h2 id="데이터베이스-및-사용자-조회">데이터베이스 및 사용자 조회</h2>
<pre><code class="language-sql">// 모든 데이터베이스 조회
SELECT DATNAME FROM PG_DATABASE;

// 사용자가 만든 데이터베이스만 조회(시스템 DB 제외)
SELECT DATNAME FROM PG_DATABASE
WHERE DATISTEMPLATE = FALSE;

// 현재 데이터베이스와 모든 테이블 조회
SELECT TABLE_SCHEMA, TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA NOT INT (&#39;pg_catalog&#39;, &#39;information_schema&#39;)
ORDER BY TABLE_SCHEMA, TABLE_NAME;

// 특정 테이블의 컬럼 정보 조회 (예. users 테이블)
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE
FROM INFORMATION_SCHEMA.columns
WHERE TABLE_NAME = &#39;users&#39;;

// 모든 사용자(Role) 조회
SELECT rolename FROM PG_ROLES;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[LLM? MCP? 그게 뭐야?]]></title>
            <link>https://velog.io/@study_panda98/LLM-MCP-%EA%B7%B8%EA%B2%8C-%EB%AD%90%EC%95%BC</link>
            <guid>https://velog.io/@study_panda98/LLM-MCP-%EA%B7%B8%EA%B2%8C-%EB%AD%90%EC%95%BC</guid>
            <pubDate>Mon, 15 Dec 2025 03:49:00 GMT</pubDate>
            <description><![CDATA[<p>LLM과 MCP라는 용어가 이제는 개발자 채용 공고에서 심심찮게 발견되는 경우가 많아졌다. 그래서 이 용어들은 다 뭘까? 싶은 사람들을 위해서 간단히 정리 해보려 한다.</p>
<p>이 용어들에 대해서 알기 전에 AI에 대해서 살짝 짚고 넘어가보자.</p>
<h1 id="ai">AI</h1>
<p>AI는 <code>Artificial Intelligence</code> 단어 그대로 인공 + 지능이다. 컴퓨터 시스템이 인간의 지능을 모방하여 학습, 추론, 문제 해결 등의 작업을 수행하는 기술을 말한다.
AI는 머신러닝 기술을 통해 학습을 하고, 데이터를 분석하며, 분석을 통해 학습하고 이 내용을 기반으로 판단 및 예측을 할 수 있는 알고리즘과 기술을 개발하는 분야를 말합니다.</p>
<h1 id="llm-large-language-model">LLM (Large Language Model)</h1>
<p> LLM은 방대한 양의 데이터를 학습하여 자연어(인간의 언어)와 복잡한 데이터를 분석/해석할 수 있는 AI 프로그램을 말합니다. 우리 주위에서 볼 수 있는 가장 쉬운 예시로는 질의응답이 가능한 <strong>생성형 AI</strong>로 <code>ChatGPT(OpenAI), Gemini(Google), Claude(Anthropic)</code> 등이 있습니다.</p>
<p> 최근에는 단순한 질의 응답을 넘어서 프로그래밍까지 가능하도록 진화되어 개발자와는 뗄래야 뗄 수 없는 AI 툴도 많이 개발되고 있습니다. 대표적인 예시로는 <code>Cursor</code>가 있겠습니다.</p>
<p> 초기의 LLM의 가장 큰 문제로 대두된 것은 바로 <strong>&#39;데이터의 고착화&#39;</strong> 가 있겠습니다. 그리고 외부와의 통신이 불가능하다는 점이죠.
 이를 해결하기 위해 나온 규약이 MCP입니다.</p>
<h1 id="mcp-model-context-protocol">MCP (Model Context Protocol)</h1>
<p> AI 모델이 외부 도구, 데이터, 시스템 등 외부와 쉽게 소통할 수 있도록 하는 표준 통신 규약(프로토콜)입니다. MCP를 이용하여 얻을 수 있는 이점으로는 할루시네이션 감소, AI 유용성 및 자동화 향상, AI를 위한 간편한 연결이 있습니다.</p>
<p> 이러한 MCP에는 LLM과 외부 시스템이 쉽게 상호작용할 수 있도록 하는 구성요소가 몇 가지 있습니다.</p>
<h2 id="mcp-host">MCP Host</h2>
<p> LLM은 AI 기반 IDE 또는 대화형AI와 같은 AI 애플리케이션 또는 환경인 MCP 호스트 내에 포함되어 있습니다. 일반적으로 사용자의 상호작용 지점이며, 호스트는 LLM을 사용하여 외부 데이터나 도구가 필요할 수 있는 요청을 처리합니다.</p>
<h2 id="mcp-client">MCP Client</h2>
<p> MCP Host 내에 있는 MCP Client는 LLM과 MCP 서버가 서로 통신하도록 도와줍니다. MCP에 대한 LLM의 요청을 변환하고 LLM에 대한 MCP의 대답을 변환합니다. 또한 사용 가능한 MCP 서버를 찾아 사용합니다.</p>
<h2 id="mcp-server">MCP Server</h2>
<p> LLM에 Context, 데이터 또는 기능을 제공하는 외부 서비스를 말합니다. DB 및 웹 서비스와 같은 외부 시스템에 연결하여 LLM의 응답을 LLM이 이해할 수 있는 형식으로 변환함으로써 개발자가 다양한 기능을 제공할 수 있도록 LLM을 지원합니다.</p>
<h2 id="전송-계층">전송 계층</h2>
<p> 전송에는 JSON-RPC 2.0이 사용됩니다. 주로 두 가지 방법이 사용되는데 로컬 리소스에 적합하며 빠른 동기식 메세지 전송을 제공하는 <code>표준 입출력(stdio)</code> 와 원격 리소스에 선호되고 효율적인 실시간 데이터 스트리밍을 지원하는 <code>서버 전송 이벤트(Server Sent Event)</code> 가 있습니다.</p>
<p>출처 | <a href="https://cloud.google.com/discover/what-is-model-context-protocol?hl=ko">Google Cloud</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[지원하지 않는 버전의 톰캣 설치 & eclipse에서 svn 연결하기]]></title>
            <link>https://velog.io/@study_panda98/%EC%A7%80%EC%9B%90%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%B2%84%EC%A0%84%EC%9D%98-%ED%86%B0%EC%BA%A3-%EC%84%A4%EC%B9%98-eclipse%EC%97%90%EC%84%9C-svn-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@study_panda98/%EC%A7%80%EC%9B%90%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%B2%84%EC%A0%84%EC%9D%98-%ED%86%B0%EC%BA%A3-%EC%84%A4%EC%B9%98-eclipse%EC%97%90%EC%84%9C-svn-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 29 Dec 2023 03:32:33 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에 투입이 되면서 클라이언트 쪽 개발환경에 맞게 로컬세팅하는 도중 난관에 봉착했어서 기록해보려한다.
기록을 안하면 까먹을 것 같았거든요.. <span style="color:grey;font-size:14px;"><del>(나도 알고 싶지 않았어ㅠㅠ)</del></span>
프로젝트 설정때문에 하루를 다 날려버리면 그렇게 허무할 수가 없어요...</p>
<h2 id="클라이언트-측-개발환경">클라이언트 측 개발환경</h2>
<p>클라이언트 측의 개발환경은 <code>Eclipse, SVN, JDK 1.7, Tomcat 7</code> 을 사용한다고 한다.</p>
<p>Git 밖에 써보지 않았는데 SVN을 이번에 처음 알게 됐네요😮</p>
<h2 id="톰캣-설치">톰캣 설치</h2>
<p>일단 웹 개발의 기본인 jdk부터 설치하고 tomcat을 깔아보자. (jdk는 구글링하면 잘 나오니까 알아서 설치!)</p>
<p>tomcat을 다운로드 받으려 Apache에 들어가니 아래 사진처럼 톰캣7은 더이상 지원을 안한단다...
<img src="https://velog.velcdn.com/images/study_panda98/post/d8bdbdd2-d12e-4d9a-9a68-cce1993d5036/image.png" alt=""></p>
<p>하지만 <strong>어림없지!!</strong> 지옥에서도 꺼내와야하는 상황이니까 방법을 찾아냈지</p>
<p><a href="https://archive.apache.org/dist/tomcat/">지원 중단된 톰캣 다운로드 링크</a></p>
<p>🔼위 사이트에 가보면 다양한 버전의 톰캣이 있으니 <strong><span style="color:red">참고!!</span></strong></p>
<h2 id="이클립스-설치">이클립스 설치</h2>
<p>이클립스는 아무 버전이나 깔아도 상관없지만 웹개발에 맞는 이클립스 패키지를 설치해야하니까 잘 보고 설치해야 합니다.</p>
<p>제대로 살피지 않고 설치하면 저처럼 다시 깔아야 할테니까요ㅜㅜ
아래 사진처럼 Web Developers라고 적혀있는 버전 설치하시면 됩니다. <strong>(2023년 12월 기준)</strong></p>
<p><img src="https://velog.velcdn.com/images/study_panda98/post/e65420c1-d00a-45b1-81fb-8e0a2516d7c6/image.png" alt="Java and Web Developers Package"></p>
<p>설치하시고 jdk 버전을 설치 버전에 맞게 적용해주셔야 합니다!</p>
<p>상단 메뉴바에서 <code>Window &gt; Preferences &gt; Java &gt; Compiler</code> 클릭하셔서 설치한 jdk 버전으로 변경해주시고 Apply and Close</p>
<p><img src="https://velog.velcdn.com/images/study_panda98/post/259e65bb-85fb-4611-83a6-8a01588edef2/image.png" alt="eclipse jdk 적용"></p>
<h3 id="svn-연결하기">SVN 연결하기</h3>
<p>이클립스에서 저장소의 프로젝트를 다운로드 받기 위해서 svn을 설치해야합니다.</p>
<p>이클립스 상단 메뉴바에서 <code>Help &gt; Eclipse Marketplace</code>에서 svn 검색하여 <strong>Subversvie SVN Team Provider</strong> 설치</p>
<p>SVN을 이용하기 위해서는 Connector도 필요하기 때문에 connector도 설치해보겠습니다.
원래 svn을 설치하면 connector도 같이 설치됐었으나 중간에 수동설치로 변경된 듯 합니다.</p>
<p>1번 방법으로 시도해봤으나 안되길래 2번 방법으로 해보니 됐네요..ㅠㅠ
<strong><u>1번 안되시는 분들은 2번도 같이 봐주세요!</u></strong></p>
<h4 id="svn-connector-설치-방법-1">SVN Connector 설치 방법 1</h4>
<p><code>Help &gt; Install New Software</code>에서 Add 클릭하셔서 Location에 아래 주소를 입력해주세요!</p>
<blockquote>
<p><a href="https://osspit.org/eclipse/subversive-connectors/">https://osspit.org/eclipse/subversive-connectors/</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/study_panda98/post/c2adc402-f2bd-423f-851a-c01e025114c9/image.png" alt="svn connectors"></p>
<p>펜딩 되면 사진처럼 Subversive SVN Connectors가 뜰테니 체크해주시고 Next 눌러서 설치해주시면 완료~!</p>
<p>라면 다행이겠지만 사내망에서는 저게 안먹어요!!! 구글이고 네이버고 다 되는데 저게 안됩니다!! 내부망은 이래서 힘든가봅니다...</p>
<h4 id="svn-connector-설치-방법-2">SVN Connector 설치 방법 2</h4>
<p>SVN Connector는 jar파일 2개만 있으면 된다는 글을 보고 위에 올려드렸던 링크로 들어가봤습니다.</p>
<p><img src="https://velog.velcdn.com/images/study_panda98/post/4942b0e6-60a0-429a-b5a5-1ce09330e11a/image.png" alt="eclipse connectors"></p>
<p>저기서 <code>plugins/</code>에 들어가셔서 두 개의 파일 모두 다운로드 받아주세요.</p>
<p><img src="https://velog.velcdn.com/images/study_panda98/post/8671e255-48b9-433a-bf25-02ec2830a4df/image.png" alt="connectors jar 파일 2개"></p>
<p>그리고 2개의 파일을 <code>(eclipse 설치 경로)/plugins</code>에 넣어주시면 됩니다.</p>
<blockquote>
<p>참고 : 보통 eclipse는 Window 기준으로 <code>C:\사용자\(사용자 이름)\eclipse</code>에 설치되어 있습니다.</p>
</blockquote>
<h4 id="svn-connector-설치-확인">SVN Connector 설치 확인</h4>
<p>Eclipse 상단 메뉴바에서 <code>Window &gt; Preferences &gt; Version Control (Team) &gt; SVN</code>에서 SVN Connector 탭을 클릭해주시면 Connector가 떠있는걸 확인하실 수 있습니다~
<img src="https://velog.velcdn.com/images/study_panda98/post/d9914b90-4803-455b-942e-7ef4c2a5029c/image.png" alt="SVN Connector 설치 성공"></p>
<p>휴 저같은 사람이 없길 바랍니다. 그럼 20000⭐</p>
<p><strong><em>참고 링크</em></strong>
<a href="https://velog.io/@joyoo1221/%EC%9D%B4%ED%81%B4%EB%A6%BD%EC%8A%A4-SVN-Connector">https://velog.io/@joyoo1221/%EC%9D%B4%ED%81%B4%EB%A6%BD%EC%8A%A4-SVN-Connector</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring Security 개념]]></title>
            <link>https://velog.io/@study_panda98/Spring-Security-%EA%B0%9C%EB%85%90%EA%B3%BC-%ED%9D%90%EB%A6%84-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@study_panda98/Spring-Security-%EA%B0%9C%EB%85%90%EA%B3%BC-%ED%9D%90%EB%A6%84-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 11 Oct 2023 14:24:17 GMT</pubDate>
            <description><![CDATA[<p>스프링 공부나 프로젝트를 하면서 거의 기본적으로 들어갔던 기능인 <code>Spring Security</code>에 대해서 한번 싹 정리를 해보려한다.</p>
<h2 id="인증authentication-vs-인가authorization">인증(Authentication) vs 인가(Authorization)</h2>
<ul>
<li>인증 (Authentication) : 내가 &#39;나&#39;임을 증명하는 것.</li>
<li>인가 (Authorization) : &#39;인증된&#39; 내가 특정 권한을 얻는 것.</li>
</ul>
<h2 id="spring-security-핵심-용어-정리">Spring Security 핵심 용어 정리</h2>
<ul>
<li>Principal (주체) : 애플리케이션 내에서 작업을 수행할 수 있는 사용자나 시스템.</li>
<li>Credential (신원 증명 정보) : 사용자를 식별하기 위한 정보.
ex) Password, Token 등</li>
<li>Authentication (인증) : 본인임을 증명할 때 Credential을 필요로 함.</li>
<li>Authorization (인가) : <code>반드시</code> 인증 후에 진행되어야 함.</li>
<li>Access Control (접근 제어) : 사용자가 애플리케이션 리소스에 접근하는 행위를 제어하는 것.</li>
</ul>
<h2 id="servlet-filter-vs-spring-security-filter">Servlet Filter vs Spring Security Filter</h2>
<p>Spring Security는 기본적으로 인증이나 인가의 과정에서 <code>Filter</code>라는 것을 통해서 작업을 진행한다.
따라서, 그전에 서블릿 필터에 대한 개념을 먼저 알고 가자.</p>
<h3 id="servlet-filter">Servlet Filter</h3>
<p><code>javax.servler.Filter</code> 인터페이스를 구현한 JAVA API.
서블릿 필터는 하나 이상의 필터들이 모여 <code>Filter Chain</code>을 형성한다.
<code>Filter Chain</code> 이후에<code>HTTPServlet</code> -&gt; <code>DispatcherServlet</code> 순서로 요청이 전달된다.
더 자세한 개념설명은 <a href="https://velog.io/@bagt/%ED%95%84%ED%84%B0%EC%99%80-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0">서블릿 필터란?</a>을 통해서 익히자.</p>
<h3 id="spring-security-filter">Spring Security Filter</h3>
<p><img src="https://velog.velcdn.com/images/study_panda98/post/6d0f09a5-9988-4284-acf1-e06cba3c913e/image.png" alt=""></p>
<h4 id="delegatingfilterproxy">DelegatingFilterProxy</h4>
<p>스프링에서 제공하는 서블릿 컨테이너와 스프링의 ApplicationContext 사이에서 브릿지 역할을 하는 필터.
(❗️하지만 스프링에서 정의한 Bean이 아님❗️)</p>
<h4 id="filterchainproxy">FilterChainProxy</h4>
<p>스프링 시큐리티에서 제공하는 특별한 필터.
FilterChainProxy를 통해 Spring Security에서 제공하는 보안 필터 작업을 수행할 수 있다.
<strong>(FilterChainProxy는 DelegatingFilterProxy로 감싸지는 Bean이다.)</strong></p>
<p>위에 설명한 것 외에 더 정확하게 알고 싶다면 <a href="https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-filterchainproxy">스프링 공식문서 - Spring Security Architecture</a> 을 참고하자.</p>
]]></description>
        </item>
    </channel>
</rss>