<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ji_zzu.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 20 Nov 2025 02:07:40 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ji_zzu.log</title>
            <url>https://velog.velcdn.com/images/ji_zzu/profile/2f635e5f-65e3-4dd7-a8ab-2d8a8dc8627a/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ji_zzu.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ji_zzu" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[AWS Lightsail에서 Spring Boot 무중단 배포 구축하기]]></title>
            <link>https://velog.io/@ji_zzu/AWS-Lightsail%EC%97%90%EC%84%9C-Spring-Boot-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ji_zzu/AWS-Lightsail%EC%97%90%EC%84%9C-Spring-Boot-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 20 Nov 2025 02:07:40 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<h3 id="기존-배포-방식의-문제점">기존 배포 방식의 문제점</h3>
<p>기존에는 Spring Boot 애플리케이션을 배포할 때 아래와 같은 방식을 사용</p>
<pre><code class="language-bash"># 1. 실행 중인 프로세스 확인
ps -ef | grep java

# 2. 프로세스 강제 종료
kill -9 &lt;PID&gt;

# 3. 새 버전 실행
cd /home/ubuntu/myapp
nohup java -jar application.jar --spring.profiles.active=prod &gt; app.log 2&gt;&amp;1 &amp;

# 4. 로그 확인
tail -f app.log</code></pre>
<p><strong>문제점:</strong></p>
<ul>
<li>서버 종료 후 새 서버 시작까지 <strong>5-10초의 중단 시간</strong> 발생</li>
<li>사용자가 페이지 접속 시 <strong>502 Bad Gateway</strong> 에러 발생</li>
<li>배포할 때마다 서비스 중단 불가피</li>
</ul>
<hr>
<blockquote>
<p>무중단 배포(Zero-Downtime Deployment)는 서비스를 중단하지 않고 새로운 버전을 배포하는 방식</p>
</blockquote>
<h2 id="해결-방법">해결 방법</h2>
<ol>
<li><strong>임시 포트로 새 버전 시작</strong>: 기존 서버가 실행 중일 때, 다른 포트로 새 버전을 먼저 띄움</li>
<li><strong>Health Check</strong>: 새 버전이 완전히 준비될 때까지 대기</li>
<li><strong>포트 전환</strong>: 기존 서버 종료 후 새 서버를 원래 포트로 재시작</li>
<li><strong>Nginx 프록시</strong>: 항상 같은 포트만 바라보도록 설정</li>
</ol>
<hr>
<h2 id="구현-과정">구현 과정</h2>
<h4 id="1-nginx-설정-파일-생성">1. Nginx 설정 파일 생성</h4>
<p>로컬 PC에서 <code>nginx_config</code> 파일 생성:</p>
<pre><code class="language-nginx">upstream app_backend {
    server 127.0.0.1:8080;
}

server { 
    listen 80; 
    server_name example.com www.example.com; 
    rewrite ^ https://$server_name$request_uri? permanent;
}

server { 
    listen 443 ssl; 
    server_name example.com;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location /api/stream {
        proxy_pass http://app_backend;
        proxy_set_header X-Real-IP $remote_addr; 
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
        proxy_set_header Host $http_host;

        proxy_buffering off;
        proxy_request_buffering off;
        proxy_cache off;
        gzip off;
        add_header X-Accel-Buffering no always; 
        proxy_read_timeout 3600s; 
        proxy_send_timeout 3600s;

        proxy_http_version 1.1;
        chunked_transfer_encoding on;
    }

    location / { 
        proxy_pass http://app_backend;
        proxy_set_header X-Real-IP $remote_addr; 
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
        proxy_set_header Host $http_host;
    }
}</code></pre>
<ul>
<li><code>upstream</code>에서 <strong>8080 포트만 지정</strong></li>
<li>Nginx는 항상 같은 포트만 바라보므로 안정적</li>
</ul>
<h4 id="nginx-설정-적용">Nginx 설정 적용</h4>
<pre><code class="language-bash"># FileZilla로 nginx_config 파일을 /home/ubuntu/myapp/에 업로드 후

# SSH에서 설정 적용
sudo cp /home/ubuntu/myapp/nginx_config /etc/nginx/sites-enabled/myapp
sudo nginx -t
sudo systemctl reload nginx</code></pre>
<hr>
<h4 id="2-무중단-배포-스크립트-작성">2. 무중단 배포 스크립트 작성</h4>
<p>로컬 PC에서 <code>deploy.sh</code> 파일 생성:</p>
<pre><code class="language-bash">#!/bin/bash

echo &quot;======================================&quot;
echo &quot;    Zero-Downtime Deployment&quot;
echo &quot;======================================&quot;
echo &quot;&quot;

# Always use port 8080 for final deployment
FINAL_PORT=8080
TEMP_PORT=8081

# Check if port 8080 is running
OLD_PID=$(lsof -ti:${FINAL_PORT})

if [ -n &quot;$OLD_PID&quot; ]; then
    echo &quot;[INFO] Port ${FINAL_PORT} is running (PID: $OLD_PID)&quot;
    echo &quot;[INFO] Starting new version on temporary port ${TEMP_PORT}...&quot;
else
    echo &quot;[INFO] No running process found&quot;
    echo &quot;[INFO] Starting directly on port ${FINAL_PORT}...&quot;
    TEMP_PORT=${FINAL_PORT}
fi

# Move to working directory
cd /home/ubuntu/myapp

# Start new version on temp port
nohup java -jar application.jar \
    --spring.profiles.active=prod \
    --server.port=${TEMP_PORT} \
    &gt; app_temp.log 2&gt;&amp;1 &amp;

NEW_PID=$!
echo &quot;[INFO] New process PID: $NEW_PID&quot;

# Health check
echo &quot;[INFO] Health checking new application...&quot;
RETRY_COUNT=0
MAX_RETRY=30

while [ $RETRY_COUNT -lt $MAX_RETRY ]; do
    sleep 2
    RETRY_COUNT=$((RETRY_COUNT + 1))

    # Check if process is alive
    if ! ps -p $NEW_PID &gt; /dev/null 2&gt;&amp;1; then
        echo &quot;[ERROR] New process failed to start!&quot;
        echo &quot;[ERROR] Check logs: tail -f app_temp.log&quot;
        exit 1
    fi

    # Check HTTP response
    HTTP_STATUS=$(curl -s -o /dev/null -w &quot;%{http_code}&quot; http://localhost:${TEMP_PORT}/ 2&gt;/dev/null)

    # Success if 2xx response
    if [ &quot;$HTTP_STATUS&quot; -ge 200 ] &amp;&amp; [ &quot;$HTTP_STATUS&quot; -lt 300 ]; then
        echo &quot;[SUCCESS] New application started successfully! (HTTP ${HTTP_STATUS})&quot;
        break
    fi

    echo &quot;  Waiting... (${RETRY_COUNT}/${MAX_RETRY}) - HTTP Status: ${HTTP_STATUS}&quot;
done

# Timeout check
if [ $RETRY_COUNT -eq $MAX_RETRY ]; then
    echo &quot;[ERROR] New application is not responding!&quot;
    echo &quot;[ERROR] Check logs: tail -f app_temp.log&quot;
    echo &quot;[ERROR] Old process will remain running.&quot;
    kill -9 $NEW_PID
    exit 1
fi

# If we used temp port, need to restart on final port
if [ &quot;$TEMP_PORT&quot; = &quot;$FINAL_PORT&quot; ]; then
    echo &quot;[INFO] Already running on port ${FINAL_PORT}&quot;
    mv app_temp.log app_${FINAL_PORT}.log 2&gt;/dev/null
else
    echo &quot;[INFO] Restarting on port ${FINAL_PORT}...&quot;

    # Terminate old process first
    if [ -n &quot;$OLD_PID&quot; ]; then
        echo &quot;[INFO] Terminating old process (PID: $OLD_PID)...&quot;
        kill -15 $OLD_PID

        # Wait for port to be free
        WAIT_COUNT=0
        while [ $WAIT_COUNT -lt 10 ]; do
            if ! lsof -ti:${FINAL_PORT} &gt; /dev/null 2&gt;&amp;1; then
                echo &quot;[SUCCESS] Port ${FINAL_PORT} is now free&quot;
                break
            fi
            sleep 1
            WAIT_COUNT=$((WAIT_COUNT + 1))
        done

        # Force kill if needed
        if lsof -ti:${FINAL_PORT} &gt; /dev/null 2&gt;&amp;1; then
            echo &quot;[WARN] Forcing termination...&quot;
            kill -9 $OLD_PID
            sleep 2
        fi
    fi

    # Stop temp process
    echo &quot;[INFO] Stopping temporary process...&quot;
    kill -15 $NEW_PID
    sleep 2

    # Start on final port
    echo &quot;[INFO] Starting on port ${FINAL_PORT}...&quot;
    nohup java -jar application.jar \
        --spring.profiles.active=prod \
        --server.port=${FINAL_PORT} \
        &gt; app_${FINAL_PORT}.log 2&gt;&amp;1 &amp;

    FINAL_PID=$!

    # Quick health check
    echo &quot;[INFO] Final health check...&quot;
    sleep 5

    if ps -p $FINAL_PID &gt; /dev/null 2&gt;&amp;1; then
        echo &quot;[SUCCESS] Application running on port ${FINAL_PORT}&quot;
        NEW_PID=$FINAL_PID
    else
        echo &quot;[ERROR] Failed to start on port ${FINAL_PORT}&quot;
        echo &quot;[ERROR] Check logs: tail -f app_${FINAL_PORT}.log&quot;
        exit 1
    fi
fi

echo &quot;&quot;
echo &quot;[SUCCESS] Zero-downtime deployment completed!&quot;
echo &quot;&quot;
echo &quot;Runtime Info:&quot;
echo &quot;  - PID: $NEW_PID&quot;
echo &quot;  - PORT: ${FINAL_PORT}&quot;
echo &quot;  - Log file: /home/ubuntu/myapp/app_${FINAL_PORT}.log&quot;
echo &quot;&quot;
echo &quot;View logs: tail -f app_${FINAL_PORT}.log&quot;
echo &quot;======================================&quot;</code></pre>
<h4 id="스크립트-동작-원리">스크립트 동작 원리</h4>
<pre><code>1단계: 임시 포트(8081)로 새 버전 시작
   ↓ (기존 8080 포트는 계속 서비스 중)

2단계: Health Check (최대 60초)
   ↓ (HTTP 200 응답까지 대기)

3단계: 기존 서버(8080) 종료
   ↓ (포트 완전히 해제될 때까지 대기)

4단계: 새 버전을 8080 포트로 재시작
   ↓

완료: 무중단 배포 성공!</code></pre><h4 id="스크립트-적용">스크립트 적용</h4>
<pre><code class="language-bash"># FileZilla로 deploy.sh 파일을 /home/ubuntu/myapp/에 업로드 후

# SSH에서 실행 권한 부여
chmod +x /home/ubuntu/myapp/deploy.sh</code></pre>
<hr>
<h4 id="3-배포-테스트">3. 배포 테스트</h4>
<h4 id="실행">실행</h4>
<pre><code class="language-bash">cd /home/ubuntu/myapp &amp;&amp; ./deploy.sh</code></pre>
<h4 id="실행-결과">실행 결과</h4>
<pre><code>======================================
    Zero-Downtime Deployment
======================================

[INFO] Port 8080 is running (PID: 12345)
[INFO] Starting new version on temporary port 8081...
[INFO] New process PID: 12346
[INFO] Health checking new application...
  Waiting... (1/30) - HTTP Status: 000
  Waiting... (2/30) - HTTP Status: 000
  ...
  Waiting... (13/30) - HTTP Status: 000
[SUCCESS] New application started successfully! (HTTP 200)
[INFO] Restarting on port 8080...
[INFO] Terminating old process (PID: 12345)...
[SUCCESS] Port 8080 is now free
[INFO] Stopping temporary process...
[INFO] Starting on port 8080...
[INFO] Final health check...
[SUCCESS] Application running on port 8080

[SUCCESS] Zero-downtime deployment completed!

Runtime Info:
  - PID: 12347
  - PORT: 8080
  - Log file: /home/ubuntu/myapp/app_8080.log

View logs: tail -f app_8080.log
======================================</code></pre><hr>
<h2 id="배포-프로세스-개선">배포 프로세스 개선</h2>
<p><strong>기존 방식 (5단계):</strong></p>
<pre><code class="language-bash">ps -ef | grep java
kill -9 &lt;PID&gt;
cd /home/ubuntu/myapp
nohup java -jar application.jar --spring.profiles.active=prod &gt; app.log 2&gt;&amp;1 &amp;
tail -f app.log</code></pre>
<p><strong>개선 후 (1단계):</strong></p>
<pre><code class="language-bash">cd /home/ubuntu/myapp &amp;&amp; ./deploy.sh</code></pre>
<h3 id="트러블슈팅">트러블슈팅</h3>
<h4 id="1-배포-중-502-에러가-발생하는-경우">1. 배포 중 502 에러가 발생하는 경우</h4>
<p><strong>원인</strong>: 새 애플리케이션이 준비되기 전에 기존 서버가 종료됨</p>
<p><strong>해결</strong>: 스크립트의 Health Check이 HTTP 200 응답을 확인한 후에만 전환하므로, 대부분 방지됨. 만약 발생한다면:</p>
<pre><code class="language-bash"># Health Check 대기 시간 늘리기
MAX_RETRY=30 → MAX_RETRY=50</code></pre>
<h4 id="2-새-버전-배포-실패-시">2. 새 버전 배포 실패 시</h4>
<p><strong>증상</strong>:</p>
<pre><code>[ERROR] New application is not responding!
[ERROR] Old process will remain running.</code></pre><p><strong>결과</strong>: 기존 버전이 계속 실행되므로 <strong>서비스는 정상</strong> </p>
<p><strong>확인</strong>:</p>
<pre><code class="language-bash">tail -f app_temp.log  # 에러 로그 확인</code></pre>
<h4 id="3-포트-충돌-문제">3. 포트 충돌 문제</h4>
<p><strong>확인</strong>:</p>
<pre><code class="language-bash">ps -ef | grep java</code></pre>
<p><strong>해결</strong>:</p>
<pre><code class="language-bash"># 불필요한 프로세스 종료
kill -9 &lt;PID&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS S3 파일 업로드 SignatureDoesNotMatch 에러 해결]]></title>
            <link>https://velog.io/@ji_zzu/AWS-S3-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-SignatureDoesNotMatch-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@ji_zzu/AWS-S3-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-SignatureDoesNotMatch-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 05 Nov 2025 01:36:29 GMT</pubDate>
            <description><![CDATA[<p><strong>Spring Boot에서 AWS S3에 파일 업로드 시 403 에러 발생:</strong>
The request signature we calculated does not match the signature you provided.
Check your key and signing method. (Service: S3, Status Code: 403)</p>
<h3 id="시도한-해결-방법들">시도한 해결 방법들</h3>
<p><strong>1. YAML 설정에 따옴표 추가</strong></p>
<ul>
<li>Secret Key에 특수문자(+, /)가 포함되어 YAML 파싱 문제 의심</li>
<li>secret-key: &quot;abcd/efghijklmnop&quot; 형태로 수정</li>
</ul>
<p><strong>2. AWS SDK v1 → v2 마이그레이션</strong></p>
<ul>
<li>블로그에서 SDK v1.12.460+ 버전에서 bucket/key 혼용 시 서명 문제 발생한다는 정보 확인</li>
<li>build.gradle에서 SDK v2로 변경
implementation platform(&#39;software.amazon.awssdk:bom:2.20.26&#39;)
implementation &#39;software.amazon.awssdk:s3&#39;</li>
</ul>
<p><strong>3. 새로운 IAM 사용자 및 Access Key 생성</strong></p>
<ul>
<li>기존 Access Key 손상 의심</li>
<li>s3_user 생성 후 AmazonS3FullAccess 권한 부여</li>
<li>새 Access Key 발급</li>
</ul>
<p><strong>4. application.yaml 중복 설정 제거</strong></p>
<ul>
<li>application.yaml에서 환경 변수로 AWS 설정을 덮어쓰고 있던 문제 발견</li>
</ul>
<h3 id="핵심-원인">핵심 원인</h3>
<p> ** AWS SDK v2의 서명 계산 과정에서:**</p>
<ol>
<li>HTTP 헤더에 metadata가 포함됨 (x-amz-meta-original-filename)</li>
<li>한국어가 URL 인코딩되면서 서명 계산에 사용되는 값이 달라짐</li>
<li>서버에서 검증 시 서명 불일치 → 403 에러</li>
</ol>
<p> ** AWS CLI로 같은 자격 증명 테스트:**
  AWS_ACCESS_KEY_ID=&quot;AKIA...&quot; <br>  AWS_SECRET_ACCESS_KEY=&quot;zgTF...&quot; <br>  aws s3 cp test.txt s3://test-buket/test.txt --region ap-northeast-2
  → 성공! (자격 증명은 문제 없음을 확인)</p>
<h3 id="최종-해결-방법">최종 해결 방법</h3>
<blockquote>
<p>  metadata에 한국어 파일명 포함으로 인한 서명 문제!</p>
</blockquote>
<p>  ❌ 문제 코드</p>
<pre><code class="language-java">PutObjectRequest putObjectRequest = PutObjectRequest.builder()
      .bucket(bucketName)
      .key(s3Key)
      .contentType(file.getContentType())
      .metadata(java.util.Map.of(&quot;original-filename&quot;, originalFilename))  // 한국어!
      .build();</code></pre>
<p>  ✅ 해결: metadata 제거</p>
<pre><code class="language-java">PutObjectRequest putObjectRequest = PutObjectRequest.builder()
      .bucket(bucketName)
      .key(s3Key)
      .contentType(file.getContentType())
      .build();
</code></pre>
<h3 id="참고-사항">참고 사항</h3>
<ol>
<li><strong>AWS SDK v2 추천:</strong> v1은 더 이상 유지보수되지 않음</li>
<li><strong>IAM 사용자 분리:</strong> S3 전용 IAM 사용자를 별도로 생성하는 것이 보안상 좋음</li>
<li><strong>metadata 사용 시 주의:</strong> 한글/특수문자 포함 시 인코딩 문제 발생 가능</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL 5.6 → 8.0.39 이전 시 이모지 깨짐 및 인증 오류 해결]]></title>
            <link>https://velog.io/@ji_zzu/MySQL-5.6-8.0.39-%EC%9D%B4%EC%A0%84-%EC%8B%9C-%EC%9D%B4%EB%AA%A8%EC%A7%80-%EA%B9%A8%EC%A7%90-%EB%B0%8F-%EC%9D%B8%EC%A6%9D-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@ji_zzu/MySQL-5.6-8.0.39-%EC%9D%B4%EC%A0%84-%EC%8B%9C-%EC%9D%B4%EB%AA%A8%EC%A7%80-%EA%B9%A8%EC%A7%90-%EB%B0%8F-%EC%9D%B8%EC%A6%9D-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Fri, 31 Oct 2025 02:03:25 GMT</pubDate>
            <description><![CDATA[<p>MySQL 5.6 DB(로컬 서버) 데이터를 AWS RDS MySQL 8.0.39로 옮기는 과정에서 문제와 해결 과정</p>
<hr>
<p><strong>MySQL Workbench의 Data Export</strong> 기능으로 덤프를 떴는데, 이모지가 전부 <code>?</code>로 깨져버림.</p>
<p>→ DB 구조는 utf8mb4인데도 덤프 과정에서 손실이 생김.
→ Workbench가 세션 인코딩을 <code>utf8</code>(3바이트)로 강제로 바꿔버림.</p>
<hr>
<h3 id="1차-문제-workbench-export-시-이모지-깨짐">1차 문제: Workbench export 시 이모지 깨짐</h3>
<p><strong>원인:</strong>
MySQL 5.6 Workbench는 <code>SET NAMES utf8</code>을 기본으로 사용해서 4바이트 문자가 잘려서 덤프됨.</p>
<p><strong>해결:</strong>
CLI(<code>mysqldump</code>)로 직접 export하기.</p>
<pre><code class="language-bash">/opt/homebrew/opt/mysql-client@8.0/bin/mysqldump \
  -h host\
  -u user \
  -p password \
  --default-character-set=utf8mb4 \
  --skip-set-charset \
  --single-transaction \
  scnew influ \
  &gt; ~/Downloads/influ_utf8mb4.sql</code></pre>
<p><strong>옵션 설명</strong></p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>--default-character-set=utf8mb4</code></td>
<td>4바이트 인코딩 강제</td>
</tr>
<tr>
<td><code>--skip-set-charset</code></td>
<td>덤프 파일 내 <code>SET NAMES utf8</code> 제거</td>
</tr>
<tr>
<td><code>--single-transaction</code></td>
<td>대용량 테이블 락 방지</td>
</tr>
</tbody></table>
<h3 id="2차문제--import-시-authentication-plugin-mysql_native_password-cannot-be-loaded">2차문제 : Import 시 <code>Authentication plugin &#39;mysql_native_password&#39; cannot be loaded</code></h3>
<p><strong>원인:</strong>
macOS에 설치된 MySQL 9.x 클라이언트(<code>brew install mysql</code>)가 5.6 서버용 인증 플러그인을 지원하지 않음.</p>
<p><strong>해결:</strong>
MySQL 8.0 클라이언트 따로 설치 후 그 버전으로 실행.</p>
<pre><code class="language-bash">brew install mysql-client@8.0</code></pre>
<p>설치 후, 다음 경로의 명령어로 실행👇</p>
<h3 id="import-명령">Import 명령</h3>
<pre><code class="language-bash">/opt/homebrew/opt/mysql-client@8.0/bin/mysql \
  -h host \
  -u user \
  -p \
  --default-character-set=utf8mb4 \
  scnew \
  &lt; ~/Downloads/influ_utf8mb4.sql</code></pre>
<p>이모지가 <code>😀 ❤️ 🫶</code> 등으로 정상 표시되면 완벽 성공!</p>
<hr>
<blockquote>
<p>**GUI(Workbench) 대신 CLI를 쓰면, 이모지도 깨지지 않고 인증도 오류 없이 MySQL 5.6 → 8.0 이관 가능</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[서브탭(collapse 메뉴)이 특정 페이지에서만 작동하지 않는 문제 해결]]></title>
            <link>https://velog.io/@ji_zzu/%EC%84%9C%EB%B8%8C%ED%83%ADcollapse-%EB%A9%94%EB%89%B4%EC%9D%B4-%ED%8A%B9%EC%A0%95-%ED%8E%98%EC%9D%B4%EC%A7%80%EC%97%90%EC%84%9C%EB%A7%8C-%EC%9E%91%EB%8F%99%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@ji_zzu/%EC%84%9C%EB%B8%8C%ED%83%ADcollapse-%EB%A9%94%EB%89%B4%EC%9D%B4-%ED%8A%B9%EC%A0%95-%ED%8E%98%EC%9D%B4%EC%A7%80%EC%97%90%EC%84%9C%EB%A7%8C-%EC%9E%91%EB%8F%99%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Thu, 23 Oct 2025 00:30:26 GMT</pubDate>
            <description><![CDATA[<h4 id="spring-boot--thymeleaf-프로젝트에서-사이드바의-서브탭collapse-메뉴이-특정-페이지에서만-작동하지-않는-에러">Spring Boot + Thymeleaf 프로젝트에서 사이드바의 서브탭(collapse 메뉴)이 특정 페이지에서만 작동하지 않는 에러</h4>
<ul>
<li>/mypage 페이지에서 사이드바 서브탭 클릭이 안됨</li>
<li>/rank/oliveyoung, /rank/amazon 페이지에서도 동일한 증상</li>
<li>/compstock/order-list 페이지에서도 서브탭이 동작하지 않음</li>
</ul>
<hr>
<h3 id="원인-분석">원인 분석</h3>
<p>  원인 1 : Bootstrap 버전 문법 혼재</p>
<ul>
<li>includes/user/sidebar.html 파일에서 Bootstrap 4와 Bootstrap 5 문법이 섞여있음. </li>
</ul>
<pre><code class="language-javascript">&lt;!-- 일부 메뉴는 Bootstrap 4 문법 --&gt;
&lt;a data-toggle=&quot;collapse&quot; data-target=&quot;#menu-review&quot;&gt;

&lt;!-- 일부 메뉴는 Bootstrap 5 문법 --&gt;
&lt;a data-bs-toggle=&quot;collapse&quot; data-bs-target=&quot;#menu-compstock&quot;&gt;</code></pre>
<p>원인 2 : 페이지별 Bootstrap 버전 불일치 </p>
<ul>
<li>대부분의 페이지: Bootstrap 5 사용</li>
<li>rank, order-list 등 일부 페이지: includes/user/head.html을 사용하며, 이 파일은 Bootstrap 4.6.0을 로드</li>
<li>Bootstrap 4에서는 data-bs-toggle 문법을 지원하지 않음</li>
</ul>
<pre><code>&lt;!-- head.html (문제가 있던 코드) --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css&quot;&gt;
&lt;!-- Bootstrap JS가 아예 없음 --&gt;</code></pre><hr>
<h3 id="해결-방법">해결 방법</h3>
<ol>
<li>sidebar.html 문법 통일</li>
</ol>
<p>-&gt; 모든 메뉴를 Bootstrap 5 문법으로 통일</p>
<pre><code>수정 전:
&lt;a class=&quot;submenu-toggle collapsed&quot;
   data-toggle=&quot;collapse&quot;
   data-target=&quot;#menu-review&quot;&gt;
수정 후:
&lt;a class=&quot;submenu-toggle collapsed&quot;
   data-bs-toggle=&quot;collapse&quot; 
   data-bs-target=&quot;#menu-review&quot;&gt;</code></pre><ol start="2">
<li>head.html Bootstrap 버전 업그레이드</li>
</ol>
<p>-&gt; includes/user/head.html 파일을 Bootstrap 5로 업그레이드</p>
<pre><code>CSS 수정:
&lt;!-- 변경 전 --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css&quot;&gt;

&lt;!-- 변경 후 --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css&quot;&gt;

JS 추가:
&lt;!-- jQuery 로드 후 Bootstrap JS 추가 --&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js&quot;&gt;&lt;/script&gt;</code></pre><hr>
<h3 id="주의">주의</h3>
<p>Bootstrap 4 → 5 마이그레이션 시 변경사항</p>
<ol>
<li>데이터 속성에 bs- 접두사 필수<ul>
<li>data-toggle → data-bs-toggle</li>
<li>data-target → data-bs-target</li>
<li>data-dismiss → data-bs-dismiss</li>
</ul>
</li>
<li>jQuery 의존성 제거<ul>
<li>Bootstrap 5는 jQuery가 필수가 아님</li>
<li>하지만 프로젝트에서 jQuery를 사용 중이라면 함께 로드 가능</li>
</ul>
</li>
<li>모달, 드롭다운, collapse 등 모든 컴포넌트에 적용</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Thymeleaf th:replace 차이 정리 ]]></title>
            <link>https://velog.io/@ji_zzu/Thymeleaf-threplace-%EC%B0%A8%EC%9D%B4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@ji_zzu/Thymeleaf-threplace-%EC%B0%A8%EC%9D%B4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 21 Oct 2025 05:06:36 GMT</pubDate>
            <description><![CDATA[<p>Thymeleaf로 공통 영역을 불러올 때 자주 쓰는 두 가지 방식</p>
<pre><code>&lt;div th:replace=&quot;~{includes/user/bottom}&quot;&gt;&lt;/div&gt;
&lt;div th:replace=&quot;~{includes/user/bottom::bottom}&quot;&gt;&lt;/div&gt;</code></pre><p>두 구문의 차이는 </p>
<p><strong>“파일 전체를 불러오느냐” vs “특정 fragment만 불러오느냐”</strong></p>
<h3 id="1-threplaceincludesuserbottom">1. th:replace=&quot;~{includes/user/bottom}&quot;</h3>
<p>파일 전체를 그대로 삽입_
includes/user/bottom.html의 최상위 엘리먼트(루트)부터 끝까지 현재 <code>&lt;div&gt;</code> 자리에 통째로 들어감_.</p>
<p>✅ 예시</p>
<pre><code>&lt;div th:replace=&quot;~{includes/user/bottom}&quot;&gt;&lt;/div&gt;

&lt;!-- includes/user/bottom.html --&gt;
&lt;div&gt;
  &lt;footer&gt;푸터 내용&lt;/footer&gt;
  &lt;script src=&quot;/js/common.js&quot;&gt;&lt;/script&gt;
&lt;/div&gt;</code></pre><p>👉 결과: <code>&lt;footer&gt;</code>와 <code>&lt;script&gt;</code> 모두 포함되어 div가 통째로 대체됨.</p>
<h3 id="2-threplaceincludesuserbottombottom">2. th:replace=&quot;~{includes/user/bottom::bottom}&quot;</h3>
<p><em>fragment(일부)만 불러옴.
bottom.html 안에서 th:fragment=&quot;bottom&quot;으로 정의된 특정 블록만 삽입함.</em></p>
<p>✅ 예시</p>
<pre><code>&lt;!-- includes/user/bottom.html --&gt;
&lt;div th:fragment=&quot;bottom&quot;&gt;
  &lt;footer&gt;푸터 내용&lt;/footer&gt;
&lt;/div&gt;

&lt;div th:fragment=&quot;chatbot&quot;&gt;
  &lt;script src=&quot;/js/chat.js&quot;&gt;&lt;/script&gt;
&lt;/div&gt;

&lt;div th:replace=&quot;~{includes/user/bottom::bottom}&quot;&gt;&lt;/div&gt;</code></pre><p>👉 결과: bottom fragment만 가져오며, chatbot은 불러오지 않음.</p>
<h3 id="3-팁">3. 팁</h3>
<ol>
<li>파일 안에 footer, script, modal 등 여러 섹션이 있을 땐 fragment 방식(::bottom)이 유지보수에 좋음.</li>
<li>단일 블록만 존재하는 단순 템플릿이라면 fragment 없이 써도 무방.</li>
<li>프로젝트 규모가 커질수록 fragment 방식 권장.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] Nginx에서 SSE(Server-Sent Events) 즉시 전송 설정하기]]></title>
            <link>https://velog.io/@ji_zzu/AWS-Nginx%EC%97%90%EC%84%9C-SSEServer-Sent-Events-%EC%A6%89%EC%8B%9C-%EC%A0%84%EC%86%A1-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ji_zzu/AWS-Nginx%EC%97%90%EC%84%9C-SSEServer-Sent-Events-%EC%A6%89%EC%8B%9C-%EC%A0%84%EC%86%A1-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 13 Aug 2025 04:57:51 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-상황">1. 문제 상황</h2>
<ul>
<li><p>서버에서 SSE(text/event-stream)로 로그를 실시간 전송하는 API가 있음.</p>
</li>
<li><p>브라우저에서는 로그가 바로 안 뜨고 지연되거나, 아예 중간에 끊기는 현상 발생.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ji_zzu/post/57ae5571-23c5-4f66-a65a-ce31f70151e3/image.png" alt=""></p>
<hr>
<p>원인은 Nginx의 기본 버퍼링 / 압축 때문에 스트리밍이 즉시 전달되지 않음.</p>
<hr>
<h2 id="2-해결-과정">2. 해결 과정</h2>
<h3 id="1-원인-분석">1) 원인 분석</h3>
<ul>
<li><p>proxy_buffering이 기본 on → 버퍼가 다 찰 때까지 응답을 모아둔다.</p>
</li>
<li><p>gzip이 on → 압축 버퍼링이 추가로 발생.</p>
</li>
</ul>
<p><strong>=&gt; SSE 특성상 한 줄 한 줄 즉시 전송이 중요한데, 이 두 옵션이 방해.</strong></p>
<h3 id="2-nginx-설정-수정">2) Nginx 설정 수정</h3>
<ul>
<li><p>/api/crawl/logs/stream 전용 location 블록을 만들고, 스트리밍 관련 옵션 추가.</p>
</li>
<li><p>80 → 443 리다이렉트도 함께 반영.</p>
<pre><code class="language-nginx"># HTTP → HTTPS 리다이렉트
server {
  listen 80;
  server_name test.co.kr;
  return 301 https://$host$request_uri;
}
</code></pre>
</li>
</ul>
<h1 id="https-서버">HTTPS 서버</h1>
<p>server {
    listen 443 ssl http2;
    server_name test.co.kr;</p>
<pre><code>ssl_certificate     /etc/letsencrypt/live/test.co.kr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/test.co.kr/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;

# ★ SSE 전용 API
location /api/crawl/logs/stream {
    proxy_pass         http://localhost:8080;
    proxy_http_version 1.1;

    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_buffering           off;   # 버퍼링 금지
    proxy_request_buffering   off;
    proxy_cache               off;
    gzip                      off;   # 압축 금지
    add_header X-Accel-Buffering no always;

    proxy_read_timeout  3600s;
    proxy_send_timeout  3600s;
    chunked_transfer_encoding on;
}

# 일반 API 및 웹
location / {
    proxy_pass         http://localhost:3000;
    proxy_set_header   Host $host;
    proxy_set_header   X-Real-IP $remote_addr;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
}</code></pre><p>}</p>
<pre><code>
## 3. 테스트 </code></pre><p>sudo nginx -t &amp;&amp; sudo systemctl reload nginx
curl -i -N &quot;<a href="https://test.co.kr/api/crawl/logs/stream?jobId=oy-xxxxxxxx&quot;">https://test.co.kr/api/crawl/logs/stream?jobId=oy-xxxxxxxx&quot;</a></p>
<pre><code>
- 정상: 응답 헤더에 Content-Type: text/event-stream와 X-Accel-Buffering: no 확인

- 본문이 data: 또는 :로 바로 전송됨


![](https://velog.velcdn.com/images/ji_zzu/post/18f4503e-0230-48d6-b744-15fa6fb364a7/image.png)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] EC2 에서 lightsail 로 서버(인스턴스) 이전하기 ]]></title>
            <link>https://velog.io/@ji_zzu/AWS-EC2-%EC%97%90%EC%84%9C-lightsail-%EB%A1%9C-%EC%84%9C%EB%B2%84%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%9D%B4%EC%A0%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ji_zzu/AWS-EC2-%EC%97%90%EC%84%9C-lightsail-%EB%A1%9C-%EC%84%9C%EB%B2%84%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%9D%B4%EC%A0%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 23 Jul 2025 06:25:09 GMT</pubDate>
            <description><![CDATA[<p>웹서비스를 운영하다 보면 점점 복잡해지는 인프라 관리와 비용 최적화 문제에 직면하게 된다. 특히 소규모 프로젝트나 예산이 제한된 서비스에서는 복잡한 서버를 과도하게 사용하는 것보다 간편하고 예측 가능한 요금의 가상 서버가 더 적합할 수 있다. AWS Lightsail은 이런 요구에 딱 맞춘 <strong>정액제 VPS</strong> 서비스로, EC2의 복잡함을 줄이면서도 AWS 생태계와 연동할 수 있다.</p>
<h3 id="1-왜-ec2에서-lightsail로-옮기게-되었나">1. 왜 EC2에서 Lightsail로 옮기게 되었나?</h3>
<p><img src="https://velog.velcdn.com/images/ji_zzu/post/5a9c3285-a9bc-4c12-a605-32c25eceabdc/image.png" alt=""></p>
<p><em>즉, 비용 예측과 절감 그리고 간편하게 관리하기 위해서이다 !</em></p>
<p><img src="https://velog.velcdn.com/images/ji_zzu/post/3ea52ebb-276f-493d-9105-eca55ca281e6/image.png" alt=""></p>
<h3 id="2-이전-전-준비-사항">2. 이전 전 준비 사항</h3>
<ul>
<li><p>서비스 규모 &amp; 요구사항 점검</p>
<pre><code>  - 동시 접속자 수, CPU·메모리 사용량, 디스크 I/O, 네트워크 트래픽</code></pre></li>
<li><p>데이터베이스 위치 결정</p>
<ul>
<li>Lightsail 관리형 DB 사용 vs. RDS 유지</li>
</ul>
</li>
<li><p>보안 그룹 &amp; 방화벽 규칙</p>
<ul>
<li>SSH(22), HTTP(80), HTTPS(443) 포트 열기</li>
</ul>
</li>
<li><p>DNS 제공자 확인</p>
<ul>
<li>Route 53 또는 외부(가비아, 카페24 등)</li>
</ul>
</li>
<li><p>백업 계획 수립</p>
<ul>
<li>이전 과정 중 데이터 손실 방지를 위해 반드시 스냅샷/덤프 수행</li>
</ul>
</li>
</ul>
<h3 id="3-마이그레이션-방법">3. 마이그레이션 방법</h3>
<p><strong>A. 파일 복사 방식 (추천)</strong></p>
<ol>
<li>새 Lightsail 인스턴스 생성</li>
<li>SSH 접속 후 rsync/scp로 코드·정적 파일 전송</li>
<li>DB는 mysqldump → Lightsail DB 또는 로컬에 복원</li>
<li>웹서버(nginx/apache)·언어 런타임 설치 후 서비스 시작</li>
</ol>
<p>✔️ 간단한 웹사이트나 애플리케이션 이전 시 가장 빠르고 직관적</p>
<p><strong>B. 스냅샷 기반 방식</strong></p>
<ol>
<li>EC2 EBS 스냅샷 생성 → S3로 Export</li>
<li>Lightsail 스냅샷으로 Import (콘솔에서 클릭 몇 번)</li>
<li>스냅샷으로 Lightsail 인스턴스 생성</li>
</ol>
<p>✔️ OS 설정·사용자 계정·패키지·데이터베이스까지 완전 복제할 때 유용
⚠️ S3 버킷, IAM 권한, 디스크 포맷 변환(vmdk) 과정이 추가로 필요</p>
<p>나는 최대한 쉽고 빠르게 이전하고 싶어서 <strong>A.파일 복사 방식</strong>을 사용해 이전하고자 한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] UI가 없는 웹사이트 크롤링 : JavaScript 함수 직접 호출]]></title>
            <link>https://velog.io/@ji_zzu/Python-UI%EA%B0%80-%EC%97%86%EB%8A%94-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%ED%81%AC%EB%A1%A4%EB%A7%81-JavaScript-%ED%95%A8%EC%88%98-%EC%A7%81%EC%A0%91-%ED%98%B8%EC%B6%9C</link>
            <guid>https://velog.io/@ji_zzu/Python-UI%EA%B0%80-%EC%97%86%EB%8A%94-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%ED%81%AC%EB%A1%A4%EB%A7%81-JavaScript-%ED%95%A8%EC%88%98-%EC%A7%81%EC%A0%91-%ED%98%B8%EC%B6%9C</guid>
            <pubDate>Tue, 22 Jul 2025 00:42:05 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ji_zzu/post/16e118cb-99f9-4a6e-9a72-8e374959e5b0/image.png" alt=""></p>
<p>Qoo10 리뷰 크롤링 시 페이징 버튼이 화면에 표시되지 않는 문제 때문에 기존의 시각적 버튼 클릭 방식 대신 다른 접근 방식을 사용했다. </p>
<p><img src="https://velog.velcdn.com/images/ji_zzu/post/c8107651-e889-4f0b-8d8b-0b7e485d8ee6/image.png" alt=""></p>
<p>공부의 축복이 끝이 없어 ~~!!!!!!! </p>
<h3 id="1-일반적인-페이징-크롤링-방식의-한계">1. 일반적인 페이징 크롤링 방식의 한계</h3>
<p>대부분의 웹 크롤링 튜토리얼에서는 <code>find_element</code>로 &quot;다음&quot; 버튼을 찾아 <code>click()</code>하는 방식으로 페이징을 처리한다.</p>
<p>하지만, 특정 웹사이트(예: Qoo10 리뷰 페이지)에서는 <strong>총 리뷰 개수는 많은데도 불구하고, 페이지 하단의 &quot;1, 2, 3, 다음&quot;과 같은 페이징 UI 요소가 아예 HTML에 렌더링되지 않는 문제</strong>가 발생할 수 있다.</p>
<p><em>이는 서버 응답 오류, JavaScript 로딩 실패, 또는 특정 조건(예: 검색 결과 없음)에 따른 의도된 동작일 수 있다고 한다.</em></p>
<p>이러한 상황에서는 <code>WebDriverWait</code>으로 <code>next_button</code>을 아무리 기다려도 찾을 수 없으므로, 기존 방식으로는 다음 페이지로 넘어갈 수 없다 ㅠㅠ </p>
<p>((개발이 빨리 끝날 줄 알았지만 ... 페이징이 안뜨는 이슈를 못찾아서 삽질만 왕창 ..)) </p>
<h3 id="2-웹사이트의-동적-페이징-원리-파악">2. 웹사이트의 동적 페이징 원리 파악</h3>
<p>페이징 UI가 없더라도, 웹사이트는 다음 페이지의 데이터를 어딘가에서 가져와야 한다. 이는 주로 <strong>AJAX(Asynchronous JavaScript and XML) 통신</strong>을 통해 이루어진다!! </p>
<p>Qoo10 리뷰 페이지의 경우, HTML 소스코드나 JavaScript 이벤트 리스너를 분석해보면 <code>javascript:opinionList(페이지번호);</code> 와 같은 JavaScript 함수 호출이 다음 페이지 로드를 담당한다는 것을 확인할 수 있었다.</p>
<blockquote>
<pre><code>&lt;a href=&quot;#&quot; onclick=&quot;javascript:opinionList(2); return false;&quot;&gt;2&lt;/a&gt;</code></pre></blockquote>
<h3 id="3-해결책-selenium의-execute_script를-활용한-javascript-함수-직접-호출">3. 해결책: Selenium의 execute_script를 활용한 JavaScript 함수 직접 호출</h3>
<p>Selenium WebDriver는 브라우저의 JavaScript 환경에서 임의의 JavaScript 코드를 실행할 수 있는 <code>driver.execute_script()</code> 메서드를 제공한다.</p>
<p>이 메서드를 활용하여, 웹사이트 내부적으로 다음 페이지를 로드하는 데 사용되는 <strong>JavaScript 함수(<code>opinionList()</code>)를 우리가 직접 호출</strong>하는 방식으로 페이지를 이동시킬 수 있다.</p>
<blockquote>
<pre><code>driver.execute_script(f&quot;javascript:opinionList({target_page});&quot;)</code></pre></blockquote>
<p>이 방식은 브라우저가 화면에 특정 요소를 렌더링하기를 기다릴 필요 없이, 필요한 액션을 직접 지시하는 방법이다. </p>
<h3 id="4-구현-상세-코드에-포함된-주요-로직">4. 구현 상세 (코드에 포함된 주요 로직)</h3>
<ul>
<li><strong>총 리뷰 개수 (<code>opinion_count</code>) 확인:</strong><ul>
<li>화면에 페이징 버튼이 없더라도, 총 리뷰 개수는 대부분 표시된다. 이 값을 파싱하여 크롤링해야 할 대략적인 총 페이지 수를 예측한다 (<code>math.ceil(total_reviews / 10)</code>).</li>
<li>이 예측치는 무한 루프에 빠지는 것을 방지하는 중요한 기준이 됩니다.</li>
</ul>
</li>
<li><strong><code>execute_script</code> 호출:</strong><ul>
<li><code>while</code> 루프 내에서 현재 <code>page</code> 번호에 <code>1</code>을 더한 <code>target_page</code>를 인자로 <code>opinionList()</code> 함수를 호출한다.</li>
<li><code>driver.execute_script(f&quot;javascript:opinionList({target_page});&quot;)</code></li>
</ul>
</li>
<li><strong>페이지 로드 및 콘텐츠 변경 대기:</strong><ul>
<li>JavaScript 함수 호출 후에는 AJAX 통신으로 새로운 데이터가 로드될 시간을 줘야 합니다 (<code>time.sleep(2)</code>).</li>
<li>더 확실한 방법은 <code>WebDriverWait</code>을 사용하여 새로운 리뷰 엘리먼트가 로드될 때까지 기다리거나, 이전 페이지의 리뷰 엘리먼트가 사라지는 것(<code>EC.staleness_of</code>)을 기다린다.</li>
</ul>
</li>
<li><strong>무한 루프 방지 로직:</strong><ul>
<li><code>last_review_text_on_previous_page</code> 변수를 사용하여, <code>execute_script</code>를 호출했음에도 불구하고 다음 페이지의 첫 번째 리뷰 내용이 이전 페이지의 마지막 리뷰 내용과 동일하다면, 더 이상 새로운 콘텐츠가 없다고 판단하고 크롤링을 종료한다.</li>
<li>미리 정해둔 <code>ABSOLUTE_MAX_PAGES</code> (예: 500페이지) 제한을 두어, 혹시 모를 로직 오류나 웹사이트의 비정상적인 동작으로 인한 무한 루프를 방지한다.</li>
</ul>
</li>
<li><strong><code>finally</code> 블록을 통한 WebDriver 종료:</strong><ul>
<li>모든 <code>try...except</code> 블록 바깥의 <code>finally</code> 블록에서 <code>driver.quit()</code>을 호출하여 크롤링 성공 여부나 오류 발생 여부와 상관없이 항상 WebDriver(브라우저)를 깔끔하게 종료하고 리소스를 해제한다. 크롤러의 안정성과 시스템 리소스 관리에 매우 중요하다.</li>
</ul>
</li>
</ul>
<h3 id="5-이-방법의-장점과-한계">5. 이 방법의 장점과 한계</h3>
<ul>
<li><strong>장점:</strong><ul>
<li>시각적 UI 요소에 의존하지 않고 동적 콘텐츠를 로드할 수 있어, 특정 웹사이트의 까다로운 페이징 문제를 해결할 수 있다.</li>
<li>불필요한 UI 상호작용 없이 직접적인 JavaScript 호출로 효율성을 높일 수 있다.</li>
</ul>
</li>
<li><strong>한계/고려사항:</strong><ul>
<li>웹사이트의 내부 JavaScript 함수 이름이나 작동 방식이 변경되면 크롤러 코드를 수정해야 한다. (유지보수 필요성)</li>
<li><code>execute_script</code>는 웹사이트의 원래 동작 방식과 다를 수 있으며 문제가 발생할 수도 있다.</li>
<li>무한 루프에 빠지지 않게 방지해야한다. </li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] 내가 까먹어서 정리하는 "웹 자동화 & 데이터 스크래핑 도구" 총정리]]></title>
            <link>https://velog.io/@ji_zzu/Python-%EB%82%B4%EA%B0%80-%EA%B9%8C%EB%A8%B9%EC%96%B4%EC%84%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-%EC%9B%B9-%EC%9E%90%EB%8F%99%ED%99%94-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91-%EB%8F%84%EA%B5%AC-%EC%B4%9D%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@ji_zzu/Python-%EB%82%B4%EA%B0%80-%EA%B9%8C%EB%A8%B9%EC%96%B4%EC%84%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-%EC%9B%B9-%EC%9E%90%EB%8F%99%ED%99%94-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91-%EB%8F%84%EA%B5%AC-%EC%B4%9D%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 10 Feb 2025 01:59:31 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ji_zzu/post/d1f48de5-501a-48b0-9a8d-5763e0507a2b/image.png" alt="">
얼떨결에 웹크롤링 과제를 맡게 되었고 ... 나는 경험이 없는데!!!
((그치만 하라면 하는게 개발자의 업무지.. 그치그치)) </p>
<p>지금까지는 코드를 짤 때 마다 ChatGpt 한테 이런 경우엔 어떤 방법을 사용해야해? 이런 경우는??? 하고 매번 질문을 던졌다.. 하지만 이런 내가 너무나도 한심해 보였을 뿐이고  .. 이제는 나 혼자서도 방법을 결정 할 수 있는 멋진 개발자로 성장하고 싶은 마음 뿐이다. 매일매일 똑같은 걸 해도 매일매일 까먹는 나를 위해 <strong>총정리</strong> 해보려고 한다. </p>
<h2 id="1-주요-용어-및-개념-정의">1. 주요 용어 및 개념 정의</h2>
<h4 id="1-웹-크롤링web-crawling">1. 웹 크롤링(Web Crawling)</h4>
<p>정의: 웹사이트의 페이지를 자동으로 탐색하고 데이터를 수집하는 기술
예시: TikTok에서 <strong>비디오 조회수</strong>를 수집하는 프로그램</p>
<h4 id="2-웹-스크래핑web-scraping">2. 웹 스크래핑(Web Scraping)</h4>
<p>정의: 웹 페이지의 HTML에서 원하는 정보를 추출하는 과정
예시: TikTok <strong>HTML 코드에서 특정 비디오 조회수</strong>를 추출</p>
<h4 id="차이점">차이점:</h4>
<p>•    <strong>웹 크롤링</strong>은 웹사이트의 여러 페이지를 자동으로 탐색하는 과정
•    <strong>웹 스크래핑</strong>은 특정 웹페이지에서 원하는 데이터를 추출하는 과정</p>
<p>→ 웹 크롤링을 하면서 웹 스크래핑을 수행할 수 있음!!
→ 웹 크롤링은 웹 스크래핑을 포함하는 상위 개념임</p>
<h2 id="2-데이터-수집-방식-비교">2. 데이터 수집 방식 비교</h2>
<p>크롤링을 하다 보면 자연스럽게 다양한 방법을 사용하게 된다.
나는 주로 <strong>Selenium, BeautifulSoup, XPath</strong> 이 세 가지를 많이 썼다.</p>
<p>이제 각각 어떤 방식이고, 언제 사용하면 좋은지 정리해보려고 한다.</p>
<h3 id="selenium-vs-beautifulsoup-vs-xpath-차이점-정리">Selenium vs BeautifulSoup vs XPath 차이점 정리</h3>
<table>
<thead>
<tr>
<th><strong>기술명</strong></th>
<th><strong>정의</strong></th>
<th><strong>특징</strong></th>
<th><strong>장점</strong></th>
<th><strong>단점</strong></th>
<th><strong>사용 예시</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Selenium</strong></td>
<td>웹 브라우저를 자동화하여 HTML을 가져옴</td>
<td>실제 브라우저처럼 동작</td>
<td>- JavaScript 실행 가능<br>- 실제 브라우저 환경에서 크롤링 가능</td>
<td>- 느림 (브라우저 실행 필요)<br>- 사이트에서 탐지될 가능성 있음</td>
<td>- 로그인이 필요한 사이트 크롤링<br>- TikTok, Instagram 등 JavaScript 기반 사이트 크롤링</td>
</tr>
<tr>
<td><strong>BeautifulSoup</strong></td>
<td>HTML 파싱 및 데이터 추출</td>
<td>정적인 HTML 분석</td>
<td>- 속도가 빠름<br>- 유지보수 쉬움</td>
<td>- JavaScript 실행 불가능</td>
<td>- 정적인 HTML에서 데이터 추출<br>- 뉴스 기사 제목/본문 크롤링</td>
</tr>
<tr>
<td><strong>XPath</strong></td>
<td>HTML 요소의 경로를 지정하여 찾는 방식</td>
<td>Selenium에서 사용</td>
<td>- 특정 요소를 정확하게 찾을 수 있음</td>
<td>- HTML 구조 변경 시 코드 수정 필요</td>
<td>- Selenium과 함께 특정 요소 찾을 때 사용</td>
</tr>
</tbody></table>
<p>결론:
•    JavaScript가 실행되는 사이트 → <strong>Selenium</strong>
•    정적인 HTML에서 빠르게 데이터 추출 → <strong>BeautifulSoup</strong>
•    특정 요소를 정확히 찾고 싶을 때 → <strong>XPath</strong></p>
<h3 id="실제-예제-코드-비교">실제 예제 코드 비교</h3>
<p>1) Selenium을 사용한 크롤링 (TikTok 조회수 크롤링 예제)</p>
<pre><code class="language-python">from selenium import webdriver
from selenium.webdriver.common.by import By
import time

options = webdriver.ChromeOptions()
options.add_argument(&quot;--headless&quot;)  # 브라우저 창 숨기기
driver = webdriver.Chrome(options=options)

driver.get(&quot;https://www.tiktok.com/@example/video/123456789&quot;)
time.sleep(5)  # 페이지 로딩 대기

# XPath를 사용하여 조회수 가져오기
views = driver.find_element(By.XPATH, &quot;//strong[@data-e2e=&#39;video-views&#39;]&quot;).text
print(&quot;조회수:&quot;, views)

driver.quit()</code></pre>
<blockquote>
<pre><code>- Selenium을 사용하여 실제 브라우저에서 TikTok 페이지를 연다. 
- find_element(By.XPATH, &quot;...&quot;)으로 조회수 요소를 찾아 데이터를 가져온다. 
- JavaScript 실행이 필요한 사이트에서 활용 가능</code></pre></blockquote>
<p>2) BeautifulSoup을 사용한 크롤링 (정적인 HTML에서 데이터 추출)</p>
<pre><code class="language-python">import requests
from bs4 import BeautifulSoup

url = &quot;https://example.com/news&quot;
response = requests.get(url)
soup = BeautifulSoup(response.text, &quot;html.parser&quot;)

# 기사 제목 가져오기
title = soup.find(&quot;h1&quot;).text
print(&quot;기사 제목:&quot;, title)</code></pre>
<blockquote>
<pre><code>- requests.get(url)을 사용하여 HTML을 가져온 후, BeautifulSoup으로 분석
- .find(&quot;h1&quot;)을 사용하여 h1 태그 안의 텍스트를 추출
- JavaScript 실행이 필요 없는 정적인 웹페이지에서 사용 가능</code></pre></blockquote>
<p>3) XPath를 활용한 데이터 추출</p>
<pre><code class="language-python">from lxml import html

html_code = &quot;&quot;&quot;&lt;html&gt;&lt;body&gt;&lt;div class=&#39;title&#39;&gt;웹 크롤링 기초&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;&quot;&quot;&quot;
tree = html.fromstring(html_code)

# XPath를 사용하여 제목 가져오기
title = tree.xpath(&quot;//div[@class=&#39;title&#39;]/text()&quot;)[0]
print(&quot;제목:&quot;, title)</code></pre>
<blockquote>
<pre><code>- lxml.html을 사용하여 HTML을 XPath 문법으로 파싱
- //div[@class=&#39;title&#39;]/text()을 사용하여 내부의 텍스트를 가져옴
- 정확한 요소를 찾을 때 사용</code></pre></blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] 크롤링 데이터 Notion DB 연동 가독성 문제 해결]]></title>
            <link>https://velog.io/@ji_zzu/Python-%ED%81%AC%EB%A1%A4%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-Notion-DB-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@ji_zzu/Python-%ED%81%AC%EB%A1%A4%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-Notion-DB-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Thu, 06 Feb 2025 08:22:20 GMT</pubDate>
            <description><![CDATA[<p>파이썬으로 크롤링을 통해 노션 DB에 저장하는 코드를 만들었다. 한 가지 문제점이 있었다면 .. </p>
<p><img src="https://velog.velcdn.com/images/ji_zzu/post/8fa8a161-ad3b-4aae-aa63-bf44e6906436/image.png" alt=""></p>
<p>이렇게 보였달까 ... 너무 가독성이 떨어지고 사진도 안보이고 !! 도저히 이미지 url 을 사진으로 나타낼 방법이 떠오르지 않았다. </p>
<h4 id="하지만-팀장님의-갑작스런-발견-덕분에-해결-">하지만 팀장님의 갑작스런 발견 덕분에 해결 !!</h4>
<p>방법은 바로 <strong>url 속성</strong>으로 가져오던 이미지를 <strong>파일과 미디어 속성</strong>으로 바꿔서 가져오는 것이다. </p>
<p>원래 가져오던 방식을 보자면 </p>
<pre><code class="language-python">&quot;Image URL&quot;: {&quot;url&quot;: product[&#39;image_url&#39;]},</code></pre>
<p>이런식으로 이미지 url을 가져와서 url 자체를 표에 집어 넣는 방식을 선택했다. </p>
<p>왜냐하면 처음에는 파일과 미디어는 무조건 사진 형태로 가져와서 표에 넣어야하고 그건 불가능하다고 생각했기 때문이다. 
(잘 알아보지 않은 나의 실수다 ..)</p>
<p>하지만 지금 수정한 코드를 보면 </p>
<pre><code class="language-python">&quot;Image URL&quot;: {
                &quot;files&quot;: [
                    {
                        &quot;name&quot;: &quot;product_image&quot;,
                        &quot;type&quot;: &quot;external&quot;,
                        &quot;external&quot;: {
                            &quot;url&quot;: product[&#39;image_url&#39;]
                        }
                    }
                ]
            },</code></pre>
<p>이런식으로 files로 url을 감싸서 가져오면 된다!! 
((이런 방법이 있다니 ㅠㅠ))</p>
<p><img src="https://velog.velcdn.com/images/ji_zzu/post/d1c56744-ba6b-45a6-93a3-f6a11c3a28b6/image.png" alt=""></p>
<p>레이아웃도 갤러리로 수정해주고, url을 file로 바꾸니 페이지 카드 미리보기로 페이지 콘텐츠인 Image URL이 보여질 수 있었다. </p>
<p>최종 수정 결과는 
<img src="https://velog.velcdn.com/images/ji_zzu/post/6643ddf5-b8d5-4a24-9989-69341d7d5b79/image.png" alt=""></p>
<p>더 가독성 좋게 바뀌었다. </p>
<p>어떻게 하면 더 가독성있는 DB가 될까 고민을 했었는데 이런 방법이 있을거라고는 상상도 못했다ㅠㅠ 
노션은 그냥 개인적인 기록용으로만 항상 사용했었는데 노션 DB 사용법을 더 익혀야겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] AxiosError {message: 'Network Error', name: 'AxiosError', code: 'ERR_NETWORK'…}]]></title>
            <link>https://velog.io/@ji_zzu/React-AxiosError-message-Network-Error-name-AxiosError-code-ERRNETWORK-%EC%82%BD%EC%A7%88-%ED%95%98%EB%8A%94-%EA%B0%9C%EB%B0%9C%EC%9E%90</link>
            <guid>https://velog.io/@ji_zzu/React-AxiosError-message-Network-Error-name-AxiosError-code-ERRNETWORK-%EC%82%BD%EC%A7%88-%ED%95%98%EB%8A%94-%EA%B0%9C%EB%B0%9C%EC%9E%90</guid>
            <pubDate>Thu, 23 Jan 2025 07:34:15 GMT</pubDate>
            <description><![CDATA[<p>react로 프론트를 구현하고 spring boot로 백을 구현해서 api로 둘을 연결하기로 했다. 나는 프론트를 맡았고, 다른 컴퓨터로 백을 구현한 상태, 나는 백과 프론트를 연결해본 경험이 처음이였기에 gpt의 힘을 빌려 연결 시작 </p>
<pre><code class="language-javascript">const api = axios.create({
  baseURL: &#39;http://localhost:8080&#39;,
  timeout: 5000,
  headers: {
    &#39;Content-Type&#39;: &#39;application/json&#39;,
  }
});</code></pre>
<p>이런식으로 axios 연결하고 </p>
<pre><code class="language-javascript">const handleSearch = async (e) =&gt; {
  const barcodePackage = e.target.value;
  setSearchQuery(barcodePackage);

  if (barcodePackage) {
    try {
      const response = await api.get(`/api/products/barcode/${barcodePackage}`);
      setFormData(response.data);
    } catch (error) {
      console.error(&quot;Error details:&quot;, error.response || error);
      alert(&#39;제품을 찾을 수 없습니다.&#39;);
    }
  }
};

const handleSubmit = async (e) =&gt; {
  e.preventDefault();
  try {
    const response = await api.patch(`/api/products/${formData.id}`, formData);
    alert(&#39;제품 정보가 업데이트되었습니다!&#39;);
    console.log(&#39;Updated Data:&#39;, response.data);
  } catch (error) {
    console.error(&#39;Error details:&#39;, error.response || error);
    alert(&#39;업데이트 중 오류가 발생했습니다.&#39;);
  }
};</code></pre>
<p>검색과, 제출 함수도 수정했다. api get 사용해서 spring boot랑 url을 맞게 설정하면 된다길래 드디어 백 프론트 연결을 해본다는 기쁜 마음으로 npm run dev 을 했지만 </p>
<blockquote>
<ol>
<li>AxiosError {message: &#39;Network Error&#39;, name: &#39;AxiosError&#39;, code: &#39;ERR_NETWORK&#39;, config: {…}, request: XMLHttpRequest, …}</li>
</ol>
</blockquote>
<ol>
<li>code: &quot;ERR_NETWORK&quot;</li>
<li>config: {transitional: {…}, adapter: Array(3), transformRequest: Array(1), transformResponse: Array(1), timeout: 5000, …}</li>
<li>message: &quot;Network Error&quot;</li>
<li>name: &quot;AxiosError&quot;</li>
<li>request: XMLHttpRequest {onreadystatechange: null, readyState: 4, timeout: 5000, withCredentials: false, upload: XMLHttpRequestUpload, …}</li>
<li>stack: &quot;AxiosError: Network Error\n at XMLHttpRequest.handleError (<a href="http://localhost:5173/node_modules/.vite/deps/axios.js?v=a5c51f36:1596:14)%5Cn">http://localhost:5173/node_modules/.vite/deps/axios.js?v=a5c51f36:1596:14)\n</a> at Axios.request (<a href="http://localhost:5173/node_modules/.vite/deps/axios.js?v=a5c51f36:2124:41)%5Cn">http://localhost:5173/node_modules/.vite/deps/axios.js?v=a5c51f36:2124:41)\n</a> at async handleSearch (<a href="http://localhost:5173/src/components/updatePage.jsx:60:26)&quot;">http://localhost:5173/src/components/updatePage.jsx:60:26)&quot;</a></li>
<li>[[Prototype]]: Error
updatePage.jsx:140 GET <a href="http://localhost:8080/api/products/barcode/8809750463767">http://localhost:8080/api/products/barcode/8809750463767</a> net::ERR_CONNECTION_REFUSED</li>
</ol>
<p>ㅇ ㅖ... 에러 발생..⭐️</p>
<p>1시간을 해결하지 못하고 react 코드, spring boot 코드 둘 다 열심히 수정해보았지만 계속계속 발생하는</p>
<h2 id="axioserror"><strong>AxiosError</strong></h2>
<p>도대체 뭐가 문제인건지 모르겠어서 둘이 머리 싸매고 고민했다ㅠㅠ </p>
<p>해결 방법으로  </p>
<ol>
<li>백엔드 서버가 8080 포트에서 실행 중인지<pre><code># Mac/Linux 포트 상태 확인 명령어
lsof -i :8080</code></pre></li>
<li>API 테스트:
Postman이나 브라우저에서 직접 API 호출 테스트:</li>
<li>프론트엔드 개발 서버가 5173 포트에서 실행 중인지</li>
<li>브라우저 콘솔에서 더 자세한 에러 메시지가 있는지</li>
</ol>
<p>아무리 확인해봐도 API 연결 잘 되어있고, 백엔드 서버 8080에서 실행 중이고, 프론트엔드 개발 서버는 5173포트에서 실행 중이고, 콘솔에는 계속 저 에러만 떠있는데 ㅠㅠㅠ 다른 에러는 없는데 ㅠㅠㅠ 도대체 뭐가 문제인건지 아무것도 모르겠던 그때 </p>
<p>문득 스쳐지나간 생각 .. </p>
<h2 id="baseurl-httplocalhost8080">baseURL: &#39;<a href="http://localhost:8080&#39;">http://localhost:8080&#39;</a></h2>
<p>설마 ... 하는 마음으로 확인해보니 localhost 로 되어있는게 아닌가 ㅠㅠㅠㅠㅠㅠ 우리는 지금 서로 다른 컴퓨터로 연결하고 싶어하는데 !!! &#39;<a href="http://localhost:8080&#39;%EB%A1%9C">http://localhost:8080&#39;로</a> 연결 백날 해봐라 .. 되냐고 !!!!!!</p>
<pre><code class="language-javascript">const api = axios.create({
  baseURL: &#39;http://백엔드_컴퓨터_IP:8080&#39;,  // 예: &#39;http://192.168.1.100:8080&#39;
  timeout: 5000,
  headers: {
    &#39;Content-Type&#39;: &#39;application/json&#39;,
  }
});</code></pre>
<p>이렇게 백엔드 컴퓨터 IP 주소를 localhost 자리에 넣어주어야 연결이 된다..,,,.... 이렇게 우리는 1시간 동안 삽질 후 다시는 헷갈리지 않게 코드를 꼼꼼하게 보게되었다..!! 이렇게 배우면서 성장하는거지 ... 나만 그런거 아니죠 ..? </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] [ERROR] Failed to fetch YouTube subscribers for squedney: [SSL] record layer failure (_ssl.c:2570)]]></title>
            <link>https://velog.io/@ji_zzu/ERROR-Failed-to-fetch-YouTube-subscribers-for-squedney-SSL-record-layer-failure-ssl.c2570</link>
            <guid>https://velog.io/@ji_zzu/ERROR-Failed-to-fetch-YouTube-subscribers-for-squedney-SSL-record-layer-failure-ssl.c2570</guid>
            <pubDate>Thu, 23 Jan 2025 07:02:17 GMT</pubDate>
            <description><![CDATA[<p>[ERROR] Failed to fetch YouTube subscribers for squedney: [SSL] record layer failure (_ssl.c:2570)</p>
<p>youtube 구독자를 불러오는데 이런 에러가 계속 발생했다. 처음 보는 에러라 해결하려고 해봤지만,, 해결 불가능ㅠㅠ GPT의 힘을 빌려보기 위해서 질문했는데 </p>
<blockquote>
<h3 id="ssl-문제-해결">SSL 문제 해결</h3>
<p>[SSL] record layer failure (_ssl.c:2570) 에러는 SSL 연결을 설정할 때 발생하는 오류입니다. 이미 몇 가지 해결 방법을 제시했지만, 여전히 문제가 발생하고 있다면 SSL 인증서를 우회하거나 다른 방법을 시도할 수 있습니다.</p>
</blockquote>
<p><strong>1) SSL 인증서 우회</strong>
아래와 같이 requests와 google-api의 SSL 인증서를 우회하도록 코드를 수정할 수 있습니다:</p>
<pre><code class="language-python">import ssl
import requests

# SSL 인증서 우회 (전체 코드에서 적용)
ssl._create_default_https_context = ssl._create_unverified_context

def send_slack_message(message):
    payload = {&quot;text&quot;: message}
    try:
        response = requests.post(SLACK_WEBHOOK_URL, json=payload, verify=False)  # SSL 인증 우회
        if response.status_code != 200:
            print(f&quot;Error sending message to Slack: {response.status_code}&quot;)
        else:
            print(&quot;Message sent to Slack successfully.&quot;)
    except Exception as e:
        print(f&quot;Failed to send message to Slack: {e}&quot;)</code></pre>
<p>위 코드는 requests와 google-api 클라이언트 모두에 SSL 우회를 적용하여 인증서 문제를 피할 수 있도록 합니다.</p>
<p><strong>2) requests와 google-api SSL 설정 수정</strong></p>
<p>또한 google-api 클라이언트에서 SSL 오류를 피하기 위해 다음 코드를 사용하여 SSL을 우회할 수 있습니다:</p>
<pre><code class="language-python">import ssl

# SSL 인증서 우회
ssl._create_default_https_context = ssl._create_unverified_context

# YouTube API 호출 부분
request = youtube.search().list(part=&quot;snippet&quot;, q=handle, order=&#39;relevance&#39;, maxResults=5)
response = request.execute()</code></pre>
<p>계속 이렇게만 말하는게 아니겠는가 ... 
하라는대로 했는데 문제 해결은 커녕 계속 같은 문제가 반복되었다. </p>
<p>심지어 새로운 에러 발생ㅠㅠ </p>
<blockquote>
<p>Python(32365,0x16c40b000) malloc: double free for ptr 0x12e29fa00
Python(32365,0x16c40b000) malloc: *** set a breakpoint in malloc_error_break to debug</p>
</blockquote>
<p>&quot;코드를 찬찬히 읽어보자!&quot; 하고 코드를 읽어보다가 갑자기 머릿속을 스쳐가는 생각</p>
<h2 id="-멀티스레드-">!!!!!!!!!!!!!!!!!!!!!! 멀티스레드 !!!!!!!!!!!!!!!!!!!!!!</h2>
<pre><code class="language-python">if __name__ == &quot;__main__&quot;:
    ids = get_ids_from_notion()
    with ThreadPoolExecutor(max_workers=1) as executor:
        # for entry in ids[&quot;Instagram&quot;]:
        #     executor.submit(get_instagram_followers, entry[&quot;id&quot;])
        for entry in ids[&quot;YouTube&quot;]:
            executor.submit(get_youtube_followers, entry[&quot;id&quot;])
        for entry in ids[&quot;TikTok&quot;]:
            executor.submit(get_tiktok_followers, entry[&quot;id&quot;])
    print(&quot;[INFO] 프로그램 종료.&quot;)</code></pre>
<p>팔로워를 자동으로 크롤링해서 업데이트 해주는 프로그램을 만들고 있었는데, 노션 DB 저장되어있는 인플루언서만 거의 4만명 ..? 이들을 다 크롤링 하려면 시간이 너무너무 오래걸려서 멀티스레드 방식을 선택했었다. </p>
<p>찾아보니 </p>
<p><strong>1. 공유 자원에 대한 경쟁</strong></p>
<p>  멀티스레드를 사용할 때, 여러 스레드가 동일한 자원(예: 메모리, 네트워크 연결 등)을 동시에 접근하려고 할 때 충돌이 발생할 수 있습니다. 이는 메모리 할당 문제나 네트워크 요청에서 발생할 수 있는 malloc 오류와 관련이 있을 수 있습니다.</p>
<p>  <strong>2. 네트워크 연결 문제</strong></p>
<p>  멀티스레드에서 동시에 여러 개의 네트워크 요청을 보내면, 요청들이 서버에서 처리되지 않거나 네트워크 제한에 걸려 SSL 오류가 발생할 수 있습니다. 이 경우, 서버가 동시에 많은 요청을 처리하는데 문제가 생겨 “SSL record layer failure” 오류나 “EOF occurred in violation of protocol” 오류가 발생할 수 있습니다.</p>
<p>  <strong>3. ThreadPoolExecutor와 Google API</strong></p>
<p>  ThreadPoolExecutor를 사용할 때 googleapiclient와 같은 외부 라이브러리가 멀티스레딩을 잘 지원하지 않는 경우가 있습니다. 라이브러리에서 멀티스레드를 처리하는 방식이 적절하지 않으면, 요청을 동시에 처리하면서 문제가 발생할 수 있습니다.</p>
<p>멀티스레드를 사용해서 에러가 발생할 수 있다는게 말이 되는 것이다!!!</p>
<p>스레드 수를 제한하면 출돌을 줄일 수 있다는 말에 max_workers=5 대신 max_workers=2로 줄여보았는데, 에러가 거짓말처럼 사라졌다. 완전히 다 사라진 것은 아니고 절반으로 줄었길래, 아예 멀티스레드를 사용하지 않도록 코드를 다시 수정하고 실행시켰더니 잘 작동되었다ㅠㅠ </p>
<p>TikTok, Instagram 에 비해 Youtube 인플루언서는 정말정말 적었기에, Youtube는 멀티스레드를 사용하지 않고, 순차 처리하게 코드를 수정했다. </p>
<pre><code class="language-python">if __name__ == &quot;__main__&quot;:
    ids = get_ids_from_notion()

    # Instagram과 TikTok은 멀티스레드 처리
    with ThreadPoolExecutor(max_workers=5) as executor:
        # Instagram과 TikTok 처리
        for entry in ids[&quot;Instagram&quot;]:
            executor.submit(get_instagram_followers, entry[&quot;id&quot;])
        for entry in ids[&quot;TikTok&quot;]:
            executor.submit(get_tiktok_followers, entry[&quot;id&quot;])

    # YouTube는 순차 처리
    for entry in ids[&quot;YouTube&quot;]:
        get_youtube_followers(entry[&quot;id&quot;])  # YouTube만 순차 처리

    print(&quot;[INFO] 프로그램 종료.&quot;)</code></pre>
<p>ㅠㅠ 아무도 날 도와줄 수 있는 사람은 없었지만 ,, 혼자 해결했을 때의 쾌감이란 ,,
그리고 너무 뿌듯했다. 인턴 생활하면서 배우는게 너무너무 많아서 너무 좋다! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Slack] Slack bot 만들기 ]]></title>
            <link>https://velog.io/@ji_zzu/Slack-Slack-bot-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@ji_zzu/Slack-Slack-bot-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 06 Jan 2025 01:59:49 GMT</pubDate>
            <description><![CDATA[<p>슬랙 채널로 스크린샷을 자동 전송해주는 프로젝트를 진행하게 되었다. 내가 까먹을까봐 정리해두고자한다. </p>
<h2 id="1-앱-생성">1. 앱 생성</h2>
<ol>
<li><p>Slack api &gt; Create an App 버튼 클릭
<a href="https://api.slack.com/apps">slack api</a> 에 들어가서 우측 초록색 버튼 Create New App을 클릭
<img src="https://velog.velcdn.com/images/ji_zzu/post/b6e56ed4-6e69-4c1a-a820-74bc24adb582/image.png" alt=""></p>
</li>
<li><p>From scratch 클릭 <img src="https://velog.velcdn.com/images/ji_zzu/post/bbbf3852-053a-40f2-a9a8-89c2440c7c27/image.png" alt=""></p>
</li>
<li><p>앱 이름 &amp; 워크스페이스 선택 
<img src="https://velog.velcdn.com/images/ji_zzu/post/ed3391b5-0baa-44f5-9b3c-74b51357da42/image.png" alt=""></p>
</li>
</ol>
<p>앱 이름과 워크스페이스 선택 후 Create App 버튼을 클릭한다. </p>
<h2 id="2-bot-생성">2. Bot 생성</h2>
<ol>
<li><p>OAuth &amp; Permissions 설정 
1) 왼쪽 바에서 <strong>OAuth &amp; Permissions</strong>를 찾아 클릭한다.
<img src="https://velog.velcdn.com/images/ji_zzu/post/a3b0e383-3853-4dbc-97f3-c886cd860894/image.png" alt="">
2) 스크롤 하다보면 Scopes 섹션이 나온다. <img src="https://velog.velcdn.com/images/ji_zzu/post/2a6639c9-e31c-49f1-88f2-daeb3ec68495/image.png" alt="">
Add an OAuth Scope 버튼을 누르고 
<img src="https://velog.velcdn.com/images/ji_zzu/post/5a2985d1-4128-42cf-9513-845885530e6b/image.png" alt="">
필요한 OAuth Scope 을 선택하면 된다. </p>
<h4 id="권한을-변경시킨-후에는-꼭-다시-app을-재설치하고-해당-채널에-bot을-초대시켜주어야만-실행이-된다">권한을 변경시킨 후에는 꼭 다시 app을 재설치하고, 해당 채널에 Bot을 초대시켜주어야만 실행이 된다.</h4>
</li>
<li><p>Install App to Workspace
페이지의 상단으로 올라가면 <img src="https://velog.velcdn.com/images/ji_zzu/post/b0330082-d265-4cd0-84ac-2bb9c54ba629/image.png" alt="">
이렇게 생긴 Install to WorkSpace 버튼을 발견할 수 있을 것이다. 
<img src="https://velog.velcdn.com/images/ji_zzu/post/b4c748b5-3d87-44ce-8c71-5cb7291bb9f1/image.png" alt="">
클릭해보면 위와 같은 권한 요청 창이 뜨는데 허용을 눌러주면 된다.
<img src="https://velog.velcdn.com/images/ji_zzu/post/49af41ca-8824-4fbd-9d27-566e184a4b39/image.png" alt="">
그러면 이렇게 토큰을 발급 받은 것을 확인 할 수 있다. </p>
</li>
</ol>
<p>발급받은 token 을 사용해서 이런식으로 코드를 작성하면 된다. </p>
<pre><code class="language-python">def send_to_slack(product_name, channel, screenshot_path, message=&quot;&quot;):
    try:
        response = client.files_upload_v2(
            channel=SLACK_CHANNEL,
            file=screenshot_path,
            initial_comment=f&quot;{product_name} ({channel}) - {message}&quot;,
        )
        if response.get(&quot;ok&quot;):
            print(f&quot;✅ Slack으로 전송 완료: {screenshot_path}&quot;)
        else:
            print(f&quot;⚠️ Slack 전송 실패: {response.get(&#39;error&#39;)}&quot;)
    except SlackApiError as e:
        print(f&quot;⚠️ Slack API 오류: {e.response[&#39;error&#39;]}&quot;)</code></pre>
<p>호출 방법이다. </p>
<pre><code class="language-python">capture_screenshot(product_name, channel, url)</code></pre>
<p>이 함수는 스크린샷을 저장한 뒤에 해당 파일을 Slack에 업로드해주는 방법으로 작동한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Slack] Incoming Webhooks와 Slack Bot의 차이점 ]]></title>
            <link>https://velog.io/@ji_zzu/Slack-Incoming-Webhooks%EC%99%80-Slack-Bot%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@ji_zzu/Slack-Incoming-Webhooks%EC%99%80-Slack-Bot%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Mon, 06 Jan 2025 01:52:22 GMT</pubDate>
            <description><![CDATA[<p>Slack 알림 서비스를 만들어야해서 Slack과 Python 연동에 대해 찾아보던 중 Incoming Webhooks와 Slack Bot 두 가지 서비스를 알게 되었고, 이 두 가지를 정리하기 위해서 글을 작성하게 되었다. 내가 헷갈려서 작성하는 글이니 .. 내 맘대로 작성할 것임.. </p>
<h2 id="1-incoming-webhooks">1. Incoming Webhooks</h2>
<p>Incoming Webhooks는 Slack 외부의 애플리케이션이나 서비스가 단순히 메시지를 전송할 수 있게 해주는 가장 기본적인 방식</p>
<p><strong>동작 방식:</strong></p>
<ul>
<li>Slack이 제공하는 Webhook URL에 HTTP POST 요청을 보내면, Slack 채널에 메시지가 전송된다.</li>
</ul>
<p><strong>특징:</strong></p>
<ul>
<li>메시지 전송만 가능</li>
<li>상호작용 불가(예: 버튼 클릭, 파일 업로드 등은 지원하지 않음)</li>
<li>설정이 간단하고, 추가적인 권한 설정이 필요하지 않음</li>
</ul>
<p><strong>사용 사례:</strong></p>
<ul>
<li>애플리케이션 상태 알림</li>
<li>간단한 로그나 알림 전송</li>
<li>빌드 결과나 배포 완료 알림</li>
</ul>
<p>내 프로젝트의 코드로 예시를 들자면 </p>
<pre><code class="language-python">def send_webhook_message(text):
    payload = {&quot;text&quot;: text}
    response = requests.post(SLACK_WEBHOOK_URL, json=payload)
    if response.status_code == 200:
        print(&quot;✅ Webhook 메시지 전송 완료&quot;)
    else:
        print(f&quot;⚠️ Webhook 메시지 전송 실패: {response.text}&quot;)</code></pre>
<p>이런식으로 간단한 상태 알림을 해주는 기능이다.</p>
<pre><code class="language-python">send_webhook_message(&quot;크롤링 작업이 완료되었습니다. 결과를 확인하세요.&quot;)</code></pre>
<p>호출할때는 이렇게 호출하면 된다. </p>
<h2 id="2-slack-bot">2. Slack Bot</h2>
<p>Slack Bot은 Slack API를 활용하여 더 복잡하고 상호작용이 가능한 기능을 제공하는 방식</p>
<p><strong>동작 방식:</strong></p>
<ul>
<li>Slack API를 사용하여 메시지 전송, 파일 업로드, 채널 정보 읽기, 사용자와 상호작용 등의 작업 수행</li>
<li>Bot Token을 통해 인증 및 권한 부여</li>
</ul>
<p><strong>특징:</strong></p>
<ul>
<li>양방향 상호작용 가능(예: 버튼 클릭 이벤트 처리, 대화형 메시지 등)</li>
<li>파일 업로드, 데이터 조회, 사용자 입력 처리 등 다양한 기능 지원</li>
<li>권한 및 범위 설정이 필요</li>
</ul>
<p><strong>사용 사례:</strong></p>
<ul>
<li>고객 지원 챗봇</li>
<li>워크플로우 자동화</li>
<li>프로젝트 관리 및 알림</li>
</ul>
<p>내 프로젝트의 코드로 예시를 들자면 </p>
<pre><code class="language-python">def send_to_slack(product_name, channel, screenshot_path, message=&quot;&quot;):
    try:
        response = client.files_upload_v2(
            channel=SLACK_CHANNEL,
            file=screenshot_path,
            initial_comment=f&quot;{product_name} ({channel}) - {message}&quot;,
        )
        if response.get(&quot;ok&quot;):
            print(f&quot;✅ Slack으로 전송 완료: {screenshot_path}&quot;)
        else:
            print(f&quot;⚠️ Slack 전송 실패: {response.get(&#39;error&#39;)}&quot;)
    except SlackApiError as e:
        print(f&quot;⚠️ Slack API 오류: {e.response[&#39;error&#39;]}&quot;)</code></pre>
<p>이런식으로 스크린샷 file을 보내주는 기능이다.</p>
<pre><code class="language-python">capture_screenshot(product_name, channel, url)</code></pre>
<p>호출할때는 이렇게 호출하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Django] Mac에서 login 페이지 만들기 ]]></title>
            <link>https://velog.io/@ji_zzu/Django-Mac%EC%97%90%EC%84%9C-login-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@ji_zzu/Django-Mac%EC%97%90%EC%84%9C-login-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 02 Jan 2025 05:18:19 GMT</pubDate>
            <description><![CDATA[<h3 id="1-url-연결하기">1. URL 연결하기</h3>
<p>웹사이트 주소(<a href="http://localhost:8000/login/)%EB%A5%BC">http://localhost:8000/login/)를</a> 생성하기 위해서는
my_project/urls.py 파일에 url 주소 추가 </p>
<pre><code class="language-python">from django.contrib import admin
from django.urls import path

urlpatterns = [
    path(&#39;admin/&#39;, admin.site.urls),
    path(&#39;login/&#39;, views.login_view, name=&#39;login&#39;),  # 로그인 페이지 추가
]</code></pre>
<h3 id="2-로그인-페이지에서의-해야-할-일-작성">2. 로그인 페이지에서의 해야 할 일 작성</h3>
<p>dm/views.py</p>
<pre><code class="language-python">from django.shortcuts import render

def login_view(request):
    return render(request, &#39;login.html&#39;)
    ```python
from django.shortcuts import render

def login_view(request):
    return render(request, &#39;login.html&#39;)
</code></pre>
<h3 id="3-html-파일-만들기">3. HTML 파일 만들기</h3>
<p>templates라는 폴더를 만들고, 그 안에 login.html 파일 생성</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;Login&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Instagram Login&lt;/h1&gt;
    &lt;form method=&quot;post&quot;&gt;
        {% csrf_token %}
        &lt;label for=&quot;username&quot;&gt;Username:&lt;/label&gt;
        &lt;input type=&quot;text&quot; id=&quot;username&quot; name=&quot;username&quot;&gt;
        &lt;label for=&quot;password&quot;&gt;Password:&lt;/label&gt;
        &lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot;&gt;
        &lt;button type=&quot;submit&quot;&gt;Login&lt;/button&gt;
    &lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h2 id="서버-실행하기">서버 실행하기</h2>
<blockquote>
<p>python manage.py runserver</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Django] Mac에서 프로젝트 생성 ]]></title>
            <link>https://velog.io/@ji_zzu/Django-Mac%EC%97%90%EC%84%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@ji_zzu/Django-Mac%EC%97%90%EC%84%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Thu, 02 Jan 2025 04:54:40 GMT</pubDate>
            <description><![CDATA[<p>Spring boot와 Python 연결만 생각해보다가 
Django를 사용해 보라는 추천을 받아 Django로 프로젝트 생성하게 되었다. </p>
<h2 id="1-django">1. Django?</h2>
<p>Django는 웹사이트를 쉽게 만들도록 도와주는 *<em>“프레임워크” *</em></p>
<p>웹사이트는 집이라고 생각해보면 이 집을 짓기 위해서는 기둥, 벽, 문 같은 기본 구조가 필요한데 Django는 이 기본 구조를 자동으로 만들어 줘서 우리가 빨리 웹사이트를 만들 수 있게 해준다. </p>
<h2 id="2-설치해야-할-것들">2. 설치해야 할 것들</h2>
<ol>
<li><p>Python </p>
<blockquote>
<p>python --version</p>
</blockquote>
</li>
<li><p>Django</p>
<blockquote>
<p>pip install django</p>
</blockquote>
</li>
</ol>
<h2 id="3-django로-프로젝트-시작">3. Django로 프로젝트 시작</h2>
<ol>
<li>Django 프로젝트 생성:<blockquote>
<p>django-admin startproject insta_dm_project</p>
</blockquote>
</li>
</ol>
<pre><code>1-1) 폴더 구조 

&gt; insta_dm_project/
├── manage.py
├── my_project/
    ├── init.py
    ├── settings.py
    ├── urls.py
    ├── asgi.py
    ├── wsgi.py


•    manage.py: Django 프로젝트를 시작하거나 관리하는 데 사용하는 파일
•    settings.py: 웹사이트에 필요한 설정(예: 언어, 시간대 등)을 저장하는 곳
•    urls.py: 웹사이트의 주소를 관리</code></pre><ol start="2">
<li>앱 만들기</li>
</ol>
<p>Django에서는 “앱”이라는 작은 기능 단위로 작업을 한다.</p>
<p>앱은 레고 블록의 개념
예를 들어, “로그인 앱”, “DM 앱”처럼 각각의 기능을 만들어 붙이는 거야.</p>
<pre><code>1) 앱 생성 </code></pre><blockquote>
<pre><code>python manage.py startapp dm</code></pre></blockquote>
<pre><code>2) 폴더 구조:
이제 dm 폴더 구조 </code></pre><blockquote>
<p> dm/
├── admin.py
├── apps.py
├── models.py
├── tests.py
├── views.py
├── init.py
└── migrations/</p>
</blockquote>
<pre><code>•    views.py: 사용자가 어떤 행동을 하면, 어떻게 반응할지 작성
•    models.py: 데이터를 저장하고 관리(예: 사용자 정보)
•    admin.py: 관리자 페이지를 관리</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Repository 정리]]></title>
            <link>https://velog.io/@ji_zzu/Spring-Boot-Repository-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@ji_zzu/Spring-Boot-Repository-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 21 Nov 2024 05:59:11 GMT</pubDate>
            <description><![CDATA[<h3 id="repository">Repository</h3>
<p>데이터베이스와 상호작용하며, 데이터의 CRUD(Create, Read, Update, Delete) 작업을 수행한다.</p>
<p>• Repository는 Spring Data JPA와 같은 ORM(Object-Relational Mapping) 기술을 사용하여 데이터베이스에 접근한다.</p>
<p>• 주로 @Repository 어노테이션이 붙은 인터페이스로 정의되며, JpaRepository와 같은 기본 인터페이스를 확장하여 사용한다. </p>
<p>• Repository 메서드를 호출하면, 데이터베이스에서 데이터를 조회하거나 저장하는 작업을 수행한다. </p>
<pre><code>package com.thc.fallsprbasic.repository;


import com.thc.fallsprbasic.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository&lt;User, Long&gt; {
    User findByUsername(String username);
    User findByUsernameAndPassword(String username, String password);
}</code></pre><h3 id="repository-생성시-필수-요소">repository 생성시 필수 요소</h3>
<ul>
<li>@Repository 어노테이션 추가</li>
<li>extends JpaRepository 추가</li>
<li>&lt;entity type, entity type의 pk(Id)type&gt; 의 규칙으로 작성</li>
</ul>
<h4 id="기본적으로-제공되는-메서드">기본적으로 제공되는 메서드</h4>
<ul>
<li>findAll() : 데이터를 조회할때 사용하는 메서드</li>
</ul>
<h4 id="기본적으로-제공되지-않는-메서드">기본적으로 제공되지 않는 메서드</h4>
<ul>
<li>findByUsername(String username) : Username으로 데이터를 조회할 때 사용하는 메서드 </li>
<li>findByUsernameAndPassword(String username, String password) : username과 password로 데이터를 조회할 때 사용하는 메서드 </li>
</ul>
<p>위와 같이 메서드를 직접 생성해서 사용할 수 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Controller 정리]]></title>
            <link>https://velog.io/@ji_zzu/Spring-Boot-Controller-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@ji_zzu/Spring-Boot-Controller-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 21 Nov 2024 05:45:45 GMT</pubDate>
            <description><![CDATA[<h3 id="controller의-종류">Controller의 종류</h3>
<h4 id="1-page-controllercontroller">1. Page Controller(@Controller)</h4>
<h4 id="2-rest-controllerrestcontroller">2. Rest Controller(@RestController)</h4>
<h2 id="1-page-controllercontroller-1">1. Page Controller(@Controller)</h2>
<h4 id="html-사용">HTML 사용</h4>
<p>Page Controller는 주로 HTML 페이지를 반환한다. 이를 통해 사용자에게 웹 페이지를 보여줄 수 있다.</p>
<h4 id="역할">역할</h4>
<p>전통적인 웹 애플리케이션에서 사용된다. 클라이언트(주로 웹 브라우저)가 요청을 보내면, 서버는 해당 요청에 맞는 HTML 페이지를 렌더링하여 클라이언트에게 반환한다.</p>
<ul>
<li>예: 사용자가 브라우저에서 <a href="http://localhost:8080/board/home%EB%A5%BC">http://localhost:8080/board/home를</a> 요청하면, Page Controller는 home.html 파일을 찾아 렌더링한 후, 이 HTML 페이지를 사용자에게 전달한다.</li>
</ul>
<h2 id="2-rest-controller-restcontroller">2. Rest Controller (@RestController)</h2>
<h4 id="html-미사용">HTML 미사용</h4>
<p>Rest Controller는 HTML을 반환하지 않는다. 대신, JSON 또는 XML 형식의 데이터를 반환한다.</p>
<h4 id="역할-1">역할</h4>
<p>RESTful API를 제공하는 데 사용된다. 주로 웹 애플리케이션의 백엔드에서 데이터를 처리하고, 이 데이터를 클라이언트(예: 웹 브라우저, 모바일 앱, 다른 서버)에 전달한다.</p>
<ul>
<li>예: 사용자가 <a href="http://localhost:8080/board/list%EB%A5%BC">http://localhost:8080/board/list를</a> 요청하면, Rest Controller는 데이터베이스에서 게시글 목록을 가져와 JSON 형식으로 반환한다. 클라이언트는 이 JSON 데이터를 사용하여 필요한 작업을 수행한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot 기본 구조]]></title>
            <link>https://velog.io/@ji_zzu/Spring-Boot-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@ji_zzu/Spring-Boot-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Tue, 19 Nov 2024 15:17:37 GMT</pubDate>
            <description><![CDATA[<h2 id="spring-boot-기본-구조">Spring Boot 기본 구조</h2>
<p><img src="https://velog.velcdn.com/images/ji_zzu/post/7506be80-cec2-4d12-a282-e6a03046a9d4/image.png" alt=""></p>
<p>Controller, Service, Repository, DB 가 존재한다. </p>
<h4 id="controller는-쇼핑몰의-웹사이트-service는-가게의-직원-repository는-물건을-보관하는-창고와-같은-역할">&lt;Controller는 쇼핑몰의 웹사이트, Service는 가게의 직원, Repository는 물건을 보관하는 창고와 같은 역할&gt;</h4>
<h2 id="controller">Controller</h2>
<h4 id="역할">역할</h4>
<p>Controller는 클라이언트로부터 들어오는 HTTP 요청을 처리하고, 적절한 서비스를 호출하여 응답을 생성한다.</p>
<h4 id="작동-방식">작동 방식</h4>
<ul>
<li>클라이언트가 브라우저 또는 API 클라이언트를 통해 HTTP 요청을 보낸다.
이 요청은 Spring Boot의 내장 웹 서버(예: Tomcat)에 의해 받아들여지며, 적절한 Controller로 라우팅된다.</li>
<li>Controller는 주로 @RestController 또는 @Controller 어노테이션이 붙은 클래스로 정의된다.</li>
<li>특정 URL 패턴과 매핑된 메서드는 @GetMapping, @PostMapping 등과 같은 어노테이션으로 정의되며, 이 메서드가 요청을 처리한다.</li>
<li>Controller는 Service 계층을 호출하여 비즈니스 로직을 처리한 후, 그 결과를 HTTP 응답으로 변환하여 클라이언트에 반환된다.</li>
</ul>
<blockquote>
<p>Controller는 사용자가 웹사이트에 들어와서 주문을 하는 것과 같다. 사용자가 웹사이트에서 “물건 구매” 버튼을 누르면, 그 요청이 Controller로 간다. Controller는 요청을 받아서, 가게 직원(Service)에게 “이 사람이 이 물건을 사고 싶어해”라고 전달한다.</p>
</blockquote>
<p><em>Controller는 사용자가 웹사이트에 입력하는 것(예: 주문)을 받아서 처리하는 역할을 한다.</em></p>
<h2 id="service">Service</h2>
<h4 id="역할-1">역할</h4>
<p>Service 계층은 비즈니스 로직을 처리하는 곳이다. 데이터 처리, 계산, 기타 비즈니스 규칙을 실행하며, Repository 계층을 호출하여 데이터베이스와 상호작용한다.</p>
<h4 id="작동-방식-1">작동 방식</h4>
<ul>
<li>Controller가 서비스 요청을 받으면, Service 계층의 메서드를 호출한다.
Service는 주로 @Service 어노테이션이 붙은 클래스로 정의되며, 비즈니스 로직을 캡슐화한다.</li>
<li>서비스 메서드는 비즈니스 로직을 수행하고, 필요 시 Repository 계층을 통해 데이터를 읽거나 쓰는 작업을 한다.</li>
<li>처리 결과를 Controller에 반환하여, 최종적으로 클라이언트에게 응답할 수 있도록 한다.</li>
</ul>
<blockquote>
<p>Service는 가게 직원이다. 이 직원은 고객의 요청을 받고, 그 요청을 처리한다. 예를 들어, 고객이 “이 물건을 사고 싶어요”라고 하면, 직원은 그 물건이 있는지 확인하고, 주문을 처리한다. 직원이 할 일은 “물건이 창고에 있는지 확인하고(DB 확인), 있다면 주문을 처리하고, 없다면 ‘재고 없음’이라고 알려주는 것”이다.</p>
</blockquote>
<p><em>Service는 모든 비즈니스 로직을 처리하는 곳이다. “물건을 사고 싶다”는 요청을 받아서, 처리하기 위해 필요한 일을 한다.</em></p>
<h2 id="daorepository">DAO(Repository)</h2>
<h4 id="역할-2">역할</h4>
<p>Repository 계층은 데이터베이스와 상호작용하며, 데이터의 CRUD(Create, Read, Update, Delete) 작업을 수행한다.</p>
<h4 id="작동-방식-2">작동 방식</h4>
<ul>
<li>Repository는 Spring Data JPA와 같은 ORM(Object-Relational Mapping) 기술을 사용하여 데이터베이스에 접근한다.</li>
<li>주로 @Repository 어노테이션이 붙은 인터페이스로 정의되며, JpaRepository와 같은 기본 인터페이스를 확장하여 사용한다.</li>
<li>Repository 메서드를 호출하면, 데이터베이스에서 데이터를 조회하거나 저장하는 작업을 수행한다.</li>
<li>이 계층은 데이터베이스와의 직접적인 상호작용을 추상화하여, 상위 계층에서 데이터 접근의 복잡성을 숨긴다.</li>
</ul>
<blockquote>
<p>Repository는 가게의 창고이다. 모든 물건(데이터)이 여기 저장되어 있다. 가게 직원(Service)은 필요한 물건이 있는지 이 창고에 물어본다. 창고에서는 “이 물건이 있습니다/없습니다”라고 대답한다. 이 창고는 물건을 꺼내주기도 하고(데이터를 읽기), 물건을 새로 넣어주기도 하고(데이터 저장), 오래된 물건을 폐기하기도 한다(데이터 삭제).</p>
</blockquote>
<p><em>Repository는 데이터베이스와 상호작용하는 곳이다. 데이터를 저장하고, 읽고, 업데이트하고, 삭제하는 역할을 한다.</em></p>
<h2 id="전체-작동-흐름">전체 작동 흐름</h2>
<ol>
<li><p>HTTP 요청이 클라이언트로부터 들어오면, Spring Boot의 Controller가 이 요청을 받아들인다.</p>
</li>
<li><p>Controller는 요청을 처리하기 위해 Service 계층의 메서드를 호출한다.</p>
</li>
<li><p>Service 계층은 비즈니스 로직을 수행하며, 필요 시 DAO(Repository)를 호출하여 DB와 상호작용한다.</p>
</li>
<li><p>Repository 계층은 DB에 접근하여 필요한 데이터를 읽어오거나 저장 작업을 수행한다.</p>
</li>
<li><p>Service 계층은 이 데이터를 이용해 비즈니스 로직을 마무리한 후, 결과를 Controller로 반환한다.</p>
</li>
<li><p>Controller는 처리 결과를 HTTP 응답으로 변환하여 클라이언트에 반환한다.</p>
</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>