<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>su_under.log</title>
        <link>https://velog.io/</link>
        <description>솨의 개발일기</description>
        <lastBuildDate>Wed, 01 Jan 2025 08:16:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>su_under.log</title>
            <url>https://velog.velcdn.com/images/su_under/profile/adfbf432-4c3f-4af8-8f99-81d4363b07be/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. su_under.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/su_under" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[멀티스레드로 성능 개선하기 🚀]]></title>
            <link>https://velog.io/@su_under/%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%93%9C%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@su_under/%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%93%9C%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 01 Jan 2025 08:16:53 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-상황-🤔">1. 문제 상황 🤔</h2>
<p>네이버 쇼핑의 상품 정보를 크롤링하는 과정에서 여러 문제점들이 발생했다. 기존 코드는 첫페이지만 크롤링이 되도록 구현되어 있었기 때문에 상품 정보가 20개밖에 저장되지 않았다. 이를 해결하기 위해 여러 페이지를 크롤링 하도록 코드를 수정했는데 이번에는 상품 정보가 너무 많다 보니 속도가 느리다는 문제점이 발생했다.
<img src="https://velog.velcdn.com/images/su_under/post/11d06f77-d649-468a-b070-1b45fd2ec0a7/image.png" alt=""></p>
<h2 id="2-멀티스레드의-도입-💡">2. 멀티스레드의 도입 💡</h2>
<p>크롤링 속도가 느린 가장 큰 이유는 <strong>순차적인 페이지 처리로 인한 긴 대기 시간</strong> 때문이다. 이를 해결하기 위해 방법을 찾아본 결과, <strong>멀티스레드</strong> 방식을 도입하기로 결정했다.</p>
<h3 id="왜-멀티스레드였나">왜 멀티스레드였나?</h3>
<p>멀티스레드는 여러 작업을 동시에 처리할 수 있게 해주는 프로그래밍 기법이다. 마치 여러 명이 동시에 일하는 것처럼…!</p>
<h3 id="멀티스레드의-장점">멀티스레드의 장점</h3>
<ul>
<li>동시 처리를 통한 속도 향상<ul>
<li>여러 페이지를 병렬로 처리 가능</li>
<li>전체 처리 시간 대폭 감소</li>
</ul>
</li>
<li>리소스 활용 최적화<ul>
<li>대기 시간을 효율적으로 활용</li>
<li>CPU와 네트워크 자원의 효율적인 사용</li>
</ul>
</li>
</ul>
<h2 id="3-구현-방법-🛠">3. 구현 방법 🛠</h2>
<h3 id="주요-구현-코드">주요 구현 코드</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class CrawlingService {
    private final PageRepository pageRepository;

    public List&lt;Page&gt; crawlPages(String productId) {
        // 5개의 스레드로 구성된 스레드 풀 생성
        ExecutorService executor = Executors.newFixedThreadPool(5);
        List&lt;Future&lt;List&lt;Page&gt;&gt;&gt; futures = new ArrayList&lt;&gt;();

        try {
            // 5개 페이지 동시 크롤링
            for (int pageNum = 1; pageNum &lt;= 5; pageNum++) {
                final int currentPage = pageNum;
                futures.add(executor.submit(() -&gt; crawlSinglePage(productId, currentPage)));
            }

                        ...

            // 결과 취합
            return collectResults(futures);
        } finally {
            executor.shutdown();
        }
    }
}</code></pre>
<h3 id="구현-내용">구현 내용</h3>
<ol>
<li><strong>스레드 풀 생성</strong></li>
</ol>
<ul>
<li><code>Executors.newFixedThreadPool(5)</code> → 동시에 실행할 스레드 수를 5개로 제한하여 시스템 리소스를 효율적으로 사용한다.</li>
</ul>
<ol>
<li><strong>동시 크롤링</strong></li>
</ol>
<ul>
<li><code>executor.submit()</code> → 각 페이지 크롤링 작업(<code>crawlSinglePage</code>)을 <strong>비동기적으로 실행</strong>한다.</li>
<li>페이지마다 각각 작업을 스레드 풀에서 처리한다.</li>
<li><code>Future</code> 객체를 활용해 비동기 작업의 결과를 나중에 확인한다.</li>
</ul>
<ol>
<li><strong>결과 취합</strong></li>
</ol>
<ul>
<li>비동기 작업의 결과를 <code>Future</code>로 받아와 모든 페이지의 데이터를 <strong>취합</strong>한다.</li>
<li>취합 과정에서 <strong>크롤링 실패</strong> 또는 <strong>작업 완료 여부</strong>를 처리할 수 있다.</li>
</ul>
<ol>
<li><strong>자원 관리</strong></li>
</ol>
<ul>
<li><code>executor.shutdown()</code> → 메모리 누수가 발생하지 않도록 크롤링 작업이 끝난 뒤에는 스레드 풀을 반드시 닫아 리소스를 반환한다.</li>
</ul>
<h2 id="4-성능-개선-결과-📊">4. 성능 개선 결과 📊</h2>
<p><img src="https://velog.velcdn.com/images/su_under/post/d1d9ebd1-f3a8-4db8-a3d2-f8d80dd43dcc/image.png" alt=""></p>
<p>결과적으로 크롤링 작업의 처리 속도와 시스템 안정성을 크게 향상시킬 수 있었다. 기존에는 하나의 페이지를 순차적으로 처리하면서 약 4초가 소요되었지만, 이를 페이지별 병렬 처리로 전환하여 처리 시간을 1초로 줄이는 데 성공했다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS에서 예외 처리 커스터마이징: 전역 예외 필터와 커스텀 예외 클래스]]></title>
            <link>https://velog.io/@su_under/NestJS%EC%97%90%EC%84%9C-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95-%EC%A0%84%EC%97%AD-%EC%98%88%EC%99%B8-%ED%95%84%ED%84%B0%EC%99%80-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%98%88%EC%99%B8-%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@su_under/NestJS%EC%97%90%EC%84%9C-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95-%EC%A0%84%EC%97%AD-%EC%98%88%EC%99%B8-%ED%95%84%ED%84%B0%EC%99%80-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%98%88%EC%99%B8-%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Mon, 02 Dec 2024 14:15:29 GMT</pubDate>
            <description><![CDATA[<p>개발을 하다 보면 예외 처리의 중요성을 실감하게 된다. 잘못된 입력 데이터, 데이터베이스 연결 오류, 혹은 서버의 예기치 못한 동작은 사용자 경험에 큰 영향을 미친다. NestJS는 이런 문제를 체계적으로 해결할 수 있는 예외 처리 기능을 제공하며, 이를 커스터마이징하면 프로젝트의 요구사항에 맞는 효과적인 처리가 가능한다. 이번 글에서는 NestJS의 예외 처리 메커니즘을 활용해 안정적이고 일관된 예외 처리 방식을 구현하는 방법을 다뤄보겠다.</p>
<h2 id="1-글로벌-예외-필터-구현하기">1. 글로벌 예외 필터 구현하기</h2>
<p>NestJS는 기본적으로 제공하는 예외 처리 메커니즘 외에 <code>@Catch()</code> 데코레이터를 활용하여 글로벌 예외 필터를 구현할 수 있다. </p>
<h3 id="catch-데코레이터의-역할">@Catch() 데코레이터의 역할</h3>
<p><code>@Catch()</code> 데코레이터는 <strong>NestJS에서 예외 필터를 정의하고 특정 유형의 예외를 처리할 수 있도록 지정하는 데 사용</strong>된다. 이 데코레이터를 통해 어떤 예외를 포착하고 처리할 것인지 선언할 수 있다.</p>
<ul>
<li><p><strong>특정 예외에만 적용하는 경우</strong></p>
<ul>
<li><p><code>@Catch(exceptionType)</code> 형태로 사용하여 특정 예외 타입에 대해 동작하는 필터를 구현할 수 있다.</p>
</li>
<li><p>예를 들어, 아래 코드는 <code>HttpException</code> 유형의 예외만 포착한다.</p>
<pre><code class="language-tsx">  @Catch(HttpException)
  export class HttpExceptionFilter implements ExceptionFilter {
      catch(exception: HttpException, host: ArgumentsHost) {
          // HttpException 처리 로직
      }
  }</code></pre>
</li>
</ul>
</li>
<li><p><strong>모든 예외에 적용하는 경우</strong></p>
<ul>
<li><p>데코레이터에 매개변수를 생략하면 모든 예외를 포착하도록 설정된다.</p>
<pre><code class="language-tsx">  @Catch()
  export class GlobalExceptionsFilter implements ExceptionFilter {
      catch(exception: unknown, host: ArgumentsHost) {
          // 모든 예외 처리 로직
      }
  }</code></pre>
</li>
</ul>
</li>
<li><p><strong>다수의 예외 처리도 가능</strong></p>
<ul>
<li><p><code>@Catch()</code>는 여러 예외를 처리하도록 설정할 수도 있다.</p>
<pre><code class="language-tsx">  @Catch(NotFoundException, UnauthorizedException)
  export class SpecificExceptionsFilter implements ExceptionFilter {
      catch(exception: HttpException, host: ArgumentsHost) {
          // NotFoundException 및 UnauthorizedException 처리
      }
  }</code></pre>
</li>
</ul>
</li>
</ul>
<p><code>@Catch()</code> 데코레이터는 특정 예외와 정의된 필터를 연결한다. 이렇게 정의된 필터는 예외가 발생할 때 자동으로 실행된다. 이제 실제로 글로벌 예외 필터를 적용해보자.</p>
<h3 id="글로벌-예외-필터-적용하기">글로벌 예외 필터 적용하기</h3>
<pre><code class="language-tsx">import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    HttpException,
    HttpStatus,
    Logger,
} from &#39;@nestjs/common&#39;;
import { Request, Response } from &#39;express&#39;;

@Catch()
export class GlobalExceptionsFilter implements ExceptionFilter {
    catch(exception: unknown, host: ArgumentsHost): void {

        const ctx = host.switchToHttp();
        const response = ctx.getResponse&lt;Response&gt;();
        const request = ctx.getRequest&lt;Request&gt;();

        // 기본 상태코드가 없는 경우, Internal Server Error로 처리
        const status =
            exception instanceof HttpException
                ? exception.getStatus()
                : HttpStatus.INTERNAL_SERVER_ERROR;

        // 기본 메시지를 커스텀 메시지로 설정
        const message =
            status === HttpStatus.INTERNAL_SERVER_ERROR
                ? &#39;서버에서 오류가 발생했습니다. 잠시 후 다시 시도해주세요.&#39;
                : (exception as HttpException).message ||
                  &#39;알 수 없는 오류가 발생했습니다.&#39;;

        // 응답 형식 설정
        response.status(status).json({
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
            message,
        });

        // 서버 로그 출력
        const logger: Logger = new Logger(&#39;GlobalExceptionsFilter&#39;);
        logger.error(exception);
    }
}</code></pre>
<p><code>@Catch()</code> 데코레이터를 통해 <code>GlobalExceptionsFilter</code>가 모든 예외(<code>unknown</code>)를 처리할 수 있도록 구현했다. <code>500 INTERNAL SERVER ERROR</code>인 경우, &#39;서버에서 오류가 발생했습니다. 잠시 후 다시 시도해주세요.&#39;라는 메시지를 반환한다. 그 외 상태 코드에서는 <code>HttpException</code>의 메시지 또는 &#39;알 수 없는 오류가 발생했습니다.&#39;라는 메시지를 반환할 것이다.</p>
<p>main.ts에 <code>app.useGlobalFilters(new GlobalExceptionsFilter())</code> 를 추가하면 필터가 전역으로 적용된다. 필터를 적용한 결과는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/su_under/post/162e3781-3453-4aec-ac74-860e8272e832/image.png" alt=""></p>
<hr>
<h2 id="2-커스텀-예외-클래스-작성하기">2. 커스텀 예외 클래스 작성하기</h2>
<p>NestJS는 예외 처리를 커스터마이징할 수 있도록 <code>HttpException</code> 클래스를 제공한다. 이를 상속받아 프로젝트에 특화된 예외를 정의하면 특정 상황에서 발생하는 예외를 명확히 구분할 수 있을 뿐만 아니라 반복되는 예외 처리 로직을 줄이고 재사용성을 높일 수 있다.</p>
<h3 id="커스텀-예외-클래스-적용하기">커스텀 예외 클래스 적용하기</h3>
<p>NestJS의 <code>HttpException</code>을 상속받아 예외를 정의한다. <code>HttpException</code>은 두 가지 주요 매개변수를 받는다:</p>
<ul>
<li><code>response</code>: 클라이언트에게 반환할 메시지</li>
<li><code>status</code>: HTTP 상태 코드</li>
</ul>
<pre><code class="language-tsx">import { HttpException, HttpStatus } from &#39;@nestjs/common&#39;;

export class NotFoundSessionException extends HttpException {
    constructor() {
        super(&#39;세션 게시물을 찾을 수 없습니다.&#39;, HttpStatus.NOT_FOUND);
    }
}

export class NotFoundEventException extends HttpException {
    constructor() {
        super(&#39;이벤트를 찾을 수 없습니다.&#39;, HttpStatus.NOT_FOUND);
    }
}

export class NotFoundUserException extends HttpException {
    constructor(userId: number) {
        super(
            `ID가 ${userId}인 사용자를 찾을 수 없습니다.`,
            HttpStatus.NOT_FOUND,
        );
    }
}</code></pre>
<p><code>super()</code> 메서드를 호출해 기본 상태 코드(<code>HttpStatus.NOT_FOUND</code>)와 메시지를 설정하였다. 예외에 더 많은 정보를 포함하고 싶다면, <code>NotFoundUserException</code> 클래스와 같이 생성자에서 매개변수를 받아 처리할 수 있다.</p>
<p>아래와 같이 리포지토리에 커스텀 예외를 적용해 보았다.</p>
<pre><code class="language-tsx">import { Prisma } from &#39;@prisma/client&#39;;
import { NotFoundSessionException } from &#39;../../../global/exception/custom.exception&#39;

@Injectable()
export class SessionRepository {

    async getSession(sessionId: number): Promise&lt;SessionEntity&gt; {
        const session: SessionEntity = await this.prisma.session.findUnique({
            where: {
                id: sessionId,
                isDeleted: false,
            },
            include: {
                user: true,
            },
        });

        if (!session) {
            throw new NotFoundSessionException();
        }
        return session;
    }
}</code></pre>
<br>

<p>필터를 적용한 결과는 다음과 같다.
<img src="https://velog.velcdn.com/images/su_under/post/040d9d0c-170d-45fb-8f53-4b0d4ce3026e/image.png" alt=""></p>
<hr>
<p>이번 글에서는 NestJS의 예외 처리 커스터마이징에 대해 살펴보았다. 글로벌 예외 필터로 전역적인 처리 방식을 설정하고, 커스텀 예외 클래스를 통해 모듈별로 특화된 예외를 정의하면 시스템 안정성과 사용자 경험을 동시에 높일 수 있다. 또한 하나의 파일에 여러개의 예외를 한 번에 관리할 수 있어 매우 편리하다. 예외 처리를 커스터마이징 해보면서 그동안 아무렇게나 구현했던 예외 처리도 효율적으로 할 수 있다는 것을 알게 됐다!.!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[husky로 git hook 설정하기]]></title>
            <link>https://velog.io/@su_under/husky%EB%A1%9C-git-hook-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@su_under/husky%EB%A1%9C-git-hook-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 01 Oct 2024 08:40:15 GMT</pubDate>
            <description><![CDATA[<h3 id="✨-git-hook이란"><strong>✨ Git Hook이란?</strong></h3>
<p><strong>Git Hook은</strong> Git과 관련한 어떤 이벤트가 발생했을 때 특정 스크립트를 실행할 수 있도록 하는 기능이다. 크게 <strong>클라이언트 훅</strong> 과 <strong>서버 훅</strong>으로 나뉘는데 <strong>클라이언트 훅</strong>은 commit, merge가 발생하거나 push가 발생하기 전 클라이언트에서 실행하는 훅이다. 반면 <strong>서버 훅</strong>은 git repository로 push가 발생했을 때 서버에서 실행하는 훅이다. 이번 포스팅에는 클라이언트 훅인 pre-commit 훅을 적용해 본 과정을 기록하려 한다.</p>
<h3 id="1-husky-설치">1. husky 설치</h3>
<p>먼저 아래의 명령어를 사용하여 husky를 설치한다. 나는 버전8을 사용하였다.</p>
<pre><code class="language-powershell">npm install -D husky # 설치
npx husky init # 시작</code></pre>
<p>husky를 설치 후 시작하면 package.json의 scripts에 <code>&quot;prepare&quot;: &quot;husky install&quot;</code> 이 추가된다. 이 명령어는 다른 사람들이 git clone을 받아서 사용할 때 자동으로 husky의 설정을 초기화하고 실행시켜주는 역할을 한다.</p>
<p><img src="https://velog.velcdn.com/images/su_under/post/8120dbe4-0828-4091-b1cd-6785a5e5e4bc/image.png" alt=""></p>
<p> 또한 husky를 설치하면 ./husky/pre-commit 파일도 생성된다. pre-commit 파일은 커밋하기 전에 실행되는 스크립트를 담고 있는 파일이다. 이 파일에 정의된 명령어들은 사용자가 git commit 명령어를 입력할 때 자동으로 실행된다.</p>
<h3 id="2-pre-commit-훅-설정">2. pre-commit 훅 설정</h3>
<p>이제 pre-commit 파일에 커밋 전 실행시킬 명령어를 추가하면 된다.</p>
<pre><code class="language-powershell">#!/usr/bin/env sh
. &quot;$(dirname -- &quot;$0&quot;)/_/husky.sh&quot;

npm test

npx prettier --cache --write .
npx eslint --cache .
npx tsc -p tsconfig.json --noEmit</code></pre>
<p>여기서 사용된 각 명령어는 다음과 같은 역할을 한다:</p>
<ul>
<li><code>npm test</code>: 커밋 전에 테스트를 실행하여 코드가 정상적으로 작동하는지 확인한다.</li>
<li><code>npx prettier --cache --write .</code>: 코드 포맷터인 Prettier를 사용해 코드 스타일을 자동으로 정리한다.</li>
<li><code>npx eslint --cache .</code>: ESLint를 통해 코드 품질을 검사한다.</li>
<li><code>npx tsc -p tsconfig.json --noEmit</code>: TypeScript 컴파일러를 사용해 타입 체크를 수행한다. <code>-noEmit</code> 플래그는 실제로 컴파일을 하지 않고 타입 체크만 하도록 한다.</li>
</ul>
<p>커밋 전(pre-commit) 외에도 필요한 상황에 맞게 파일 이름을 만들고 명령어를 적으면 된다.</p>
<ul>
<li>ex) push 전에 실행하고 싶은 명령어가 있다면 ./husky/pre-push 파일을 생성후 명령어를 적으면 된다.</li>
</ul>
<h3 id="❗️husky-최신-버전의-차이점">❗️husky 최신 버전의 차이점</h3>
<p>이전 버전에서는 여러 명령어를 등록하기 위해선 ’&amp;&amp;’로 한 줄에 이어 적어야 했다.</p>
<pre><code class="language-bash"># 터미널
 npx husky add .husky/pre-commit &quot;npm run format &amp;&amp; git add .&quot;</code></pre>
<p>처음에는 이 점을 모르고 최신 버전에서 명령어를 등록하다가 아래와 같은 오류가 계속 발생하였다.</p>
<pre><code class="language-powershell">add command is deprecated</code></pre>
<p>최신 버전에서는 알아서 pre-commit 파일이 생성되고 거기에 명령어를 등록하면 되기 때문에 훨씬 간편해졌다.</p>
<h3 id="결론"><strong>결론</strong></h3>
<p>husky를 활용한 Git Hook 설정은 프로젝트의 코드 품질을 높이고, 협업 시 발생할 수 있는 문제를 줄이는 데 큰 도움이 된다. 특히, 팀원들이 서로 다른 개발 환경에서도 일관된 코드 스타일과 품질 기준을 유지할 수 있도록 도와준다. 이러한 기능을 통해 발생할 수 있는 오류를 사전에 방지하고, 코드 리뷰 시간을 단축시킬 수 있기 때문에 프로젝트를 진행할 때 husky를 사용해보는 것도 좋을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis로 캐싱 구현하기]]></title>
            <link>https://velog.io/@su_under/Redis%EB%A1%9C-%EC%BA%90%EC%8B%B1-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-f5fegtea</link>
            <guid>https://velog.io/@su_under/Redis%EB%A1%9C-%EC%BA%90%EC%8B%B1-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-f5fegtea</guid>
            <pubDate>Tue, 18 Jun 2024 06:29:48 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서는 Redis로 캐싱을 구현하는 방법에 대해서 알아보겠다.
현재 진행하고 있는 OMD 프로젝트에서 매번 데스크 셋업 전체를 조회하는 것은 비효율적이라는 생각이 들어 캐싱을 적용하기로 했다.</p>
<h3 id="1-의존성-추가">1. 의존성 추가</h3>
<pre><code class="language-jsx">     implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code></pre>
<p>먼저 의존성을 추가하고 application.yml에 다음과 같이 작성한다.</p>
<pre><code class="language-yaml">spring:
  data:
    redis:
      host: redis
      port: 6379
      ssl:
        enabled: true</code></pre>
<br>

<h3 id="2-redisconfig-작성">2. RedisConfig 작성</h3>
<p>RedisConfig 파일을 작성하여 Redis를 사용하기 위한 설정을 정의해준다. Redis 서버와의 연결, 데이터 직렬화, 캐시 관리를 설정해주었는데 각자 상황에 맞게 작성하면 된다.</p>
<pre><code class="language-java">@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
    }

    @Bean
    public RedisTemplate&lt;String, Object&gt; redisTemplate() {
        RedisTemplate&lt;String, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();

        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        return redisTemplate;
    }

    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory){
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        Map&lt;String, RedisCacheConfiguration&gt; redisCacheConfigurationMap = new HashMap&lt;&gt;();

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(connectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .withInitialCacheConfigurations(redisCacheConfigurationMap)
                .build();
    }
}</code></pre>
<br>

<h3 id="3-캐싱-적용">3. 캐싱 적용</h3>
<p>마지막으로 캐싱을 적용해보자. 캐싱을 적용하고 싶은 메소드에 @Cacheable 어노테이션을 적용한다. 나는 page, limit, criteria 파라미터를 조합하여 캐시 키를 생성했다. 이로써 동일한 파라미터로 메서드가 호출되면 캐시된 결과를 반환하게 된다.</p>
<p>새로운 게시글이 생성된 후 게시글 목록 조회를 했을 경우에 최신 데이터를 반영해야 하기 때문에 @CacheEvict 적용하여 게시글이 생성될 때마다 게시글 목록 조회와 관련된 모든 캐시 항목을 제거하여 이후의 조회 요청이 최신 데이터를 반영하도록 하였다.</p>
<pre><code class="language-java">      // 게시글 생성
    @Transactional
    @CacheEvict(value = &quot;posts&quot;, allEntries = true)
    public Post savePost(PostRequest request) {
        // 게시글 생성 로직
        return post;
    }

    // 게시글 목록 조회
    @Transactional(readOnly = true)
    @Cacheable(value = &quot;posts&quot;, key = &quot;#page + &#39;-&#39; + #limit + &#39;-&#39; + #criteria&quot;)
    public List&lt;PostPreviewResponse&gt; list(Integer page, Integer limit, Integer criteria) {
        // 게시글 목록 조회 로직
    }</code></pre>
<br>

<h3 id="4-결과">4. 결과</h3>
<p><img src="https://velog.velcdn.com/images/su_under/post/af7d99ed-1794-4ebf-8def-fa450f926f46/image.png" alt=""> 캐싱 전에는 10번의 호출을 했을 때 평균적으로 30초가 걸렸다면 캐싱 후에는 평균적으로 14초가 걸렸다. 따라서 지연 시간이 대략 50%정도 감소했다. </p>
<br>

<h2 id="❗️-캐싱-구현-과정에서-있었던-오류">❗️ 캐싱 구현 과정에서 있었던 오류</h2>
<blockquote>
<p>Could not write JSON: Java 8 date/time type java.time.LocalDateTime not supported by default: add Module</p>
</blockquote>
<p>게시글 요청 응답 DTO에서 <code>LocalDateTime</code> 을 사용하고 있었는데, 해당 오류는 Java 8의 <code>LocalDateTime</code> 타입이 기본적으로 JSON 직렬화/역직렬화를 지원하지 않기 때문에 발생하는 문제였다. 이 문제를 해결하기 위해 <code>LocalDateTime</code>을 지원하는 모듈을 추가해야 했다. 따라서 다음과 같은 의존성을 추가하였다. <code>jackson-datatype-jsr310</code> 모듈은 Java 8 날짜 및 시간 API 타입을 직렬화 및 역직렬화할 수 있도록 지원한다.</p>
<pre><code class="language-jsx">implementation &#39;com.fasterxml.jackson.datatype:jackson-datatype-jsr310&#39;</code></pre>
<br>

<p>하지만 위의 의존성을 추가해도 문제가 해결되지 않았다. 찾아보니 직렬화, 역직렬화 어노테이션을 추가해 주면 문제가 해결된다는 사례를 보고 나도 동일하게 적용해 보았다.</p>
<br>

<pre><code class="language-java">public class PostResponse {
    ...

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy-MM-dd HH:mm&quot;, timezone = &quot;Asia/Seoul&quot;)
    private final LocalDateTime createdAt;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy-MM-dd HH:mm&quot;, timezone = &quot;Asia/Seoul&quot;)
    private final LocalDateTime updatedAt;

}</code></pre>
<p>위와 같이 <code>@JsonSerialize(using = LocalDateTimeSerializer.class)</code> , <code>@JsonDeserialize(using = LocalDateTimeDeserializer.class)</code> 을 추가해주었더니 문제가 해결되었다.</p>
<br>

<p>직렬화 및 역직렬화 과정을 명시적으로 정의함으로써, Jackson 라이브러리가 LocalDateTime 타입의 데이터를 처리할 때 발생할 수 있는 오류를 방지하고, 원하는 형식과 타임존에 맞게 데이터를 정확하게 변환할 수 있게 되었기 때문에 오류가 해결 된 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서킷 브레이커(CircuitBreaker) 적용하기]]></title>
            <link>https://velog.io/@su_under/%EC%84%9C%ED%82%B7-%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%BB%A4CircuitBreaker-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@su_under/%EC%84%9C%ED%82%B7-%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%BB%A4CircuitBreaker-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 04 Jun 2024 13:52:29 GMT</pubDate>
            <description><![CDATA[<p><strong>Resilience4j</strong> 라이브러리를 사용하여 서킷 브레이커를 적용하는 방법에 대해 알아보자.</p>
<h3 id="왜-굳이-resilience4j">왜 굳이 Resilience4j?</h3>
<p>Resilience4j는 함수형 프로그래밍으로 설계된 <strong>경량 장애 허용 라이브러리</strong>로, <strong>서킷 브레이커 패턴</strong>을 위해 사용된다. 서킷 브레이커 라이브러리에는 Hystrix도 있는데, Hystrix와 달리 Resilience4j는 다른 라이브러리의 의존성이 없어 가볍기 때문에 Resilience4j를 사용하였다. 참고로 Resilience4j는 CircuitBreaker이외에도 TimeLimiter, Retry등과 같은 다양한 장애 대응 패턴을 제공한다.</p>
<ul>
<li><strong>CircuitBreaker</strong> : CircuitBreaker 패턴을 구현하며, 서비스 호출의 장애를 모니터링하고 지정된 임계값 이상의 실패가 발생하면 서비스 호출을 차단하여 더 많은 장애를 방지한다.</li>
<li><strong>TimeLimiter</strong> : 서비스 호출의 최대 실행 시간을 제한하여 서비스 호출이 지나치게 길게 실행되는 것을 방지한다.</li>
<li><strong>Retry</strong> : 재시도 메커니즘을 제공하여 서비스 호출 중 장애가 발생했을 때 자동으로 재시도하고, 지수 백오프 등의 전략을 사용하여 재시도 간격을 조절할 수 있다.</li>
</ul>
<br>

<h2 id="서킷-브레이커-적용">서킷 브레이커 적용</h2>
<h3 id="1-의존성-추가">1. 의존성 추가</h3>
<pre><code class="language-javascript">      implementation &#39;io.github.resilience4j:resilience4j-spring-boot3:2.2.0&#39;
      implementation &#39;org.springframework.boot:spring-boot-starter-actuator&#39;
      implementation &#39;org.springframework.boot:spring-boot-starter-aop&#39;
      implementation &#39;io.github.resilience4j:resilience4j-circuitbreaker:2.2.0&#39;
//      implementation &#39;io.github.resilience4j:resilience4j-timelimiter:2.2.0&#39;
//      implementation &#39;io.github.resilience4j:resilience4j-retry:2.2.0&#39;</code></pre>
<p>timelimiter와 retry도 필요하다면 추가해서 사용하면 된다.</p>
<br>

<h3 id="2-applicationyml-설정">2. application.yml 설정</h3>
<pre><code class="language-yaml">resilience4j:
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true
        slidingWindowType: TIME_BASED
        slidingWindowSize: 10
        minimumNumberOfCalls: 10
        slowCallRateThreshold: 100
        slowCallDurationThreshold: 60000
        failureRateThreshold: 50
        permittedNumberOfCallsInHalfOpenState: 10
        waitDurationInOpenState: 10s
    instances:
      searchProductCircuitBreaker:
        baseConfig: default
management:
  endpoint:
    health:
      show-details: always
  endpoints:
    web:
      exposure:
        include:
          - &quot;*&quot;
  health:
    circuitbreakers:
      enabled: true</code></pre>
<ul>
<li><strong>slidingWindowType</strong>: sliding window 타입을 결정한다. COUNT_BASED인 경우 slidingWindowSize만큼의 마지막 call들이 기록되고 집계된다. TIME_BASED인 경우 마지막 slidingWindowSize초 동안의 call들이 기록되고 집계된다.</li>
<li><strong>slidingWindowSize</strong>: CLOSED 상태에서 집계되는 슬라이딩 윈도우 크기를 설정한다. (기본값: 100)</li>
<li><strong>minimumNumberOfCalls</strong>: minimumNumberOfCalls 이상의 요청이 있을 때부터 failure/slowCall rate를 계산한다. 예를들어, 해당값이 10이라면 최소한 호출을 10번을 기록해야 실패 비율을 계산할 수 있다. (기본값: 100)</li>
<li><strong>slowCallRateThreshold</strong>: 임계값을 백분율로 설정, 서킷 브레이커는 호출에 걸리는 시간이 slowCallDurationThreshold보다 길면 느린 호출로 간주, 해당 값을 넘어갈 시 서킷 브레이커는 OPEN 상태로 전환되며, 이때부터 호출을 차단한다. (기본값: 100)</li>
<li><strong>slowCallDurationThreshold</strong>: 호출에 소요되는 시간이 설정한 임계치보다 길면 느린 호출로 계산한다. (기본값: 60000[ms])</li>
<li><strong>failureRateThreshold</strong>: 실패 비율 임계치를 백분율로 설정. 해당 값을 넘어갈 시 서킷 브레이커는 OPEN 상태로 전환되며, 이때부터 호출을 차단한다. (기본값: 50)</li>
<li><strong>permittedNumberOfCallsInHalfOpenState</strong>: HALF_OPEN 상태일 때, OPEN / CLOSE 여부를 판단하기 위해 허용할 호출 횟수를 설정한다. (기본값: 10)</li>
<li><strong>waitDurationInOpenState</strong>: OPEN 상태에서 HALF_OPEN 상태로 전환하기 전 기다리는 시간 (기본값: 60000[ms])</li>
</ul>
<br>

<h3 id="3-서킷-브레이커-적용">3. 서킷 브레이커 적용</h3>
<pre><code class="language-java"> @CircuitBreaker(name = &quot;searchProductCircuitBreaker&quot;, fallbackMethod = &quot;fallback&quot;)
    public String searchProduct(String query, int display) {
        String text = URLEncoder.encode(query, StandardCharsets.UTF_8);
        String apiURL = &quot;https://openapi.naver.com/v1/search/shop.json?query=&quot; + text + &quot;&amp;display=&quot; + display;

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(apiURL))
                .header(&quot;X-Naver-Client-Id&quot;, clientId)
                .header(&quot;X-Naver-Client-Secret&quot;, clientSecret)
                .GET()
                .build();

         ...
    }

    public String fallback(String query, int display, Throwable t) {
        return &quot;Fallback! exception type: &quot; + t.getClass() + &quot;, message: &quot; + t.getMessage();
    }</code></pre>
<ul>
<li>외부 API를 호출하는 메서드 위에 <code>@CircuitBreaker</code> 어노테이션을 작성하여 application.yml에서 선언한 &quot;searchProductCircuitBreaker&quot; 인스턴스 명을 삽입한다.</li>
<li>서킷이 open되면 실행할 메서드를 fallbackMethod로 선언한다.</li>
<li>fallbackMethod는 해당하는 메서드 파리미터(String query, int display)도 fallbackMethod 파라미터로 같이 지정해줘야 한다.</li>
</ul>
<br>

<h3 id="4-결과">4. 결과</h3>
<p>요청 실패 비율이 임계치를 넘어가면 다음과 같이 에러 메시지가 뜨면서 서킷 브레이커가 OPEN 상태가 된다.</p>
<img src="https://velog.velcdn.com/images/su_under/post/7614ae88-1b19-4413-a92b-727e4e760b3b/image.png" style="margin-top: -10px;">

<br>

<p>{domain}/actuator/health 엔드포인트를 통해 서킷 브레이커의 상태를 확인할 수 있다.</p>
<img src="https://velog.velcdn.com/images/su_under/post/de1f2263-1826-41b7-973c-2a2daaa095ab/image.png" style="margin-top: -10px;">

<br>

<p>{domain}/actuator/circuitbreakerevents에서는 각각의 요청에 대한 상세한 로그를 볼 수 있다.</p>
<img src="https://velog.velcdn.com/images/su_under/post/cf3ba84d-5769-4a2a-97a3-733107b2e39f/image.png" style="margin-top: -10px;">

<br>

<h2 id="❗️resilience4j의-버전으로-인한-에러">❗️resilience4j의 버전으로 인한 에러</h2>
<p><img src="https://velog.velcdn.com/images/su_under/post/179897a0-77e6-4976-9e19-c404970dde0a/image.png" alt=""> 서킷 브레이커를 적용할 때 application.yml 파일에서 <code>show-details: always</code> 로 설정했음에도 불구하고 서킷 브레이커의 세부사항들이 보이지 않는 문제가 발생했었다. 알고 보니 resilience4j의 버전 문제였다. 만약 같은 오류가 발생했다면 공식 문서에서 안정화된 최신 버전을 사용해보길 바란다.</p>
<br>

<h2 id="🔗-참고-자료"><strong>🔗 참고 자료</strong></h2>
<ul>
<li><a href="https://bkjeon1614.tistory.com/711">https://bkjeon1614.tistory.com/711</a></li>
<li><a href="https://mangkyu.tistory.com/290">https://mangkyu.tistory.com/290</a></li>
</ul>
<br>

<h3 id="👉-관련-포스팅">👉 관련 포스팅</h3>
<p><a href="https://velog.io/@su_under/%EC%84%9C%ED%82%B7-%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%BB%A4CircuitBreaker%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1%EA%B3%BC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC">1. 서킷 브레이커(CircuitBreaker)의 필요성과 동작 원리</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서킷 브레이커(CircuitBreaker)의 필요성과 동작 원리]]></title>
            <link>https://velog.io/@su_under/%EC%84%9C%ED%82%B7-%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%BB%A4CircuitBreaker%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1%EA%B3%BC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@su_under/%EC%84%9C%ED%82%B7-%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%BB%A4CircuitBreaker%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1%EA%B3%BC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Tue, 21 May 2024 13:35:50 GMT</pubDate>
            <description><![CDATA[<h2 id="1-circuitbreaker가-필요한-이유">1. CircuitBreaker가 필요한 이유?</h2>
<p>개발을 하다 보면 외부 API를 호출해야 하는 경우가 있다. 특히나 전체적인 시스템 구성이 MSA(Microservice Architecture)로 되어 있다면 다른 서비스를 호출하는 경우가 매우 빈번하다. 문제는 서버들에 장애가 발생할 수 있다는 점인데, 호출한 다른 서비스에 장애가 발생했다면 장애가 전파되어, 해당 서비스까지 문제가 발생할 수 있다. 또한 장애가 발생한 서버에 계속 요청을 보내는 것은 장애 복구를 힘들게 만든다.</p>
<p>이를 해결하기 위해서는 <strong>장애가 발생한 서비스를 탐지</strong>하고, <strong>요청을 보내지 않도록 차단</strong>할 필요가 있다. 이때 사용되는 것이 서킷 브레이커이다.</p>
<br>

<h2 id="2-circuitbreaker란">2. CircuitBreaker란?</h2>
<p><img src="https://velog.velcdn.com/images/su_under/post/b15054f9-fbd5-4bb9-9dc4-2d3bd198db7c/image.png" alt=""></p>
<ul>
<li>서킷 브레이커는 해석 그대로 <strong>누전 차단기</strong>라는 뜻을 지닌다. 누전 차단기는 <strong>전기 회로에서 과부하가 걸리거나 단락으로 인한 피해를 막기 위해 자동으로 회로를 정지시키는 장치</strong>이다.</li>
<li>서버에서 사용하는 서킷 브레이커도 <strong>외부 API 통신의 장애 전파를 막기 위해 장애를 탐지하면 외부와의 통신을 차단하는 역할</strong>을 한다.</li>
<li>서킷 브레이커가 실행(오픈)되면 <a href="https://delf-lee.github.io/post/fail-fast/">fail-fast</a> <strong>함으로써 외부 서비스가 장애가 나더라도 빠르게 에러를 응답</strong> 받을 수 있는 장점이 있으며 개발자가 지정한 행위를 리턴 받을 수 있다.</li>
</ul>
<br>

<h2 id="3-circuitbreaker-동작-원리">3. CircuitBreaker 동작 원리</h2>
<h3 id="서킷-브레이커의-3가지-상태">서킷 브레이커의 3가지 상태</h3>
<p>실제 회로를 기준으로 전구가 외부 API 또는 Callee에 해당하고, Power Source가 클라이언트(다른 서버를 호출하는 서버) 또는 Caller에 해당한다. 그리고 회로 차단기에는 크게 CLOSED, OPEN, HALF_OPEN 3가지 상태가 존재하는데, 각각의 상태를 정리하면 다음과 같다.</p>
<ul>
<li><strong>CLOSED</strong>: circuit이 닫힌 상태로, 외부 호출이 정상상태이다.</li>
<li><strong>OPEN</strong>: circuit이 열린 상태로, 오류(실패호출/느린호출)가 발생했을 때 OPEN 상태로 전환된다.</li>
<li><strong>HALF_OPEN</strong>: OPEN 상태가 발생한 이후 일정 시간이 지난 상태이다. 이때 상태가 정상적이라면 CLOSED로, 아니라면 다시 OPEN으로 전환된다.</li>
</ul>
<br>

<h3 id="circuitbreaker의-동작-방식">CircuitBreaker의 동작 방식</h3>
<p><img src="https://velog.velcdn.com/images/su_under/post/b8147302-4873-41de-bfe7-2950070db6a5/image.png" alt=""></p>
<ol>
<li><p>일반적으로 외부 서버는 정상적으로 실행 중이며, 서킷 브레이커는 닫혀 있어 모든 요청이 정상적으로 전달되고 응답을 받는다.</p>
</li>
<li><p>외부 서버에 장애가 발생하여 요청이 실패하기 시작한다.</p>
</li>
<li><p>요청 실패가 계속되면, 설정된 실패율 임계값을 초과하게 되고 서킷 브레이커가 열려(OPEN 상태) 모든 요청이 즉시 실패한다.</p>
</li>
<li><p>서킷 브레이커가 열린 상태에서는 이후의 모든 요청이 외부 서버로 전달되지 않고 즉시 에러 또는 실패 응답을 반환한다. 이로써 외부 서버에 추가적인 부하를 방지한다.</p>
</li>
<li><p>외부 서버가 정상적으로 복구된다.</p>
</li>
<li><p>서킷 브레이커가 OPEN 상태로 전환된 후 설정된 시간이 지나면 HALF_OPEN 상태로 변경된다. 이 상태에서는 일부 요청만 외부 서버로 전달하여 서버가 정상적으로 동작하는지 테스트한다.</p>
</li>
<li><p>HALF_OPEN 상태에서 일부 요청이 성공하면, 서킷 브레이커는 다시 닫혀(CLOSED 상태) 모든 요청이 정상적으로 전달된다. 만약 요청이 실패하면 다시 OPEN 상태로 전환된다.</p>
</li>
<li><p>외부 서버가 정상적으로 응답하기 시작하면, 서킷 브레이커는 CLOSED 상태로 전환되어 모든 요청이 정상적으로 전달된다.</p>
</li>
</ol>
<br>

<h2 id="🔗-참고-자료"><strong>🔗 참고 자료</strong></h2>
<ul>
<li><a href="https://resilience4j.readme.io/docs/circuitbreaker">https://resilience4j.readme.io/docs/circuitbreaker</a></li>
<li><a href="https://mangkyu.tistory.com/261">https://mangkyu.tistory.com/261</a></li>
</ul>
<br>

<h3 id="👉-관련-포스팅">👉 관련 포스팅</h3>
<p><a href="https://velog.io/@su_under/%EC%84%9C%ED%82%B7-%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%BB%A4CircuitBreaker-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0">2. 서킷 브레이커(CircuitBreaker) 적용하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[크롤링이란? (+ 정적 크롤링 구현 예시)]]></title>
            <link>https://velog.io/@su_under/%ED%81%AC%EB%A1%A4%EB%A7%81%EC%9D%B4%EB%9E%80-%EC%A0%95%EC%A0%81-%ED%81%AC%EB%A1%A4%EB%A7%81-%EA%B5%AC%ED%98%84-%EC%98%88%EC%8B%9C</link>
            <guid>https://velog.io/@su_under/%ED%81%AC%EB%A1%A4%EB%A7%81%EC%9D%B4%EB%9E%80-%EC%A0%95%EC%A0%81-%ED%81%AC%EB%A1%A4%EB%A7%81-%EA%B5%AC%ED%98%84-%EC%98%88%EC%8B%9C</guid>
            <pubDate>Tue, 07 May 2024 12:50:55 GMT</pubDate>
            <description><![CDATA[<h1 id="웹-크롤링">웹 크롤링</h1>
<p>크롤링은 웹 상에 존재하는 자료들을 특정한 방식을 사용하여 수집하는 것을 의미한다. 여기서 중요한 점은 웹 상의 정보에는 여러가지 종류가 있다라는 점이다.</p>
<p>정적인 문서가 대상이 될 수도 있고, API와 같은 서비스가 될 수도 있다. 정적인 자료를 대상으로 하는 것은 크롤링을 한번만 해서 정리하면 되는 것이기 때문에 비교적 수월하지만 동적으로 계속해서 변화하는 자료들은 주기적으로 update를 필요로 하는 경우가 많기 때문에 특정한 방식들이 사용된다.</p>
<br>

<h2 id="정적-크롤링과-동적-크롤링">정적 크롤링과 동적 크롤링</h2>
<p><img src="https://velog.velcdn.com/images/su_under/post/eb177083-f633-437e-9c4d-9ebb2784f0ed/image.png" alt=""></p>
<p>이제 정적 크롤링과 동적 크롤링에 대해서 알아보자.</p>
<blockquote>
<p><strong>“정적 크롤링”</strong>
한 페이지 내에서 원하는 정보가 전부 드러나는 정적인 데이터를 크롤링하여 가져오는 것</p>
</blockquote>
<p>동적크롤링은 다음과 같다.</p>
<blockquote>
<p><strong>“동적 크롤링”</strong>
여러 페이지를 이동하며 마우스로 클릭하거나 타이핑을 통한 입력으로 확인할 수 있는 데이터를 가져오는 것</p>
</blockquote>
<br>

<h3 id="정적-크롤링-vs-동적-크롤링">정적 크롤링 vs 동적 크롤링</h3>
<p>대표적으로 다음과 같은 특징이 있다.</p>
<table>
<thead>
<tr>
<th></th>
<th>정적 크롤링</th>
<th>동적 크롤링</th>
</tr>
</thead>
<tbody><tr>
<td>라이브러리</td>
<td>jsoup / beautifulsoup</td>
<td>chromedriver / Selenium</td>
</tr>
<tr>
<td>속도</td>
<td>직관적으로 빠름</td>
<td>느림</td>
</tr>
<tr>
<td>비고</td>
<td>수집한 데이터를 직관적으로 빠르게 확인 가능</td>
<td>실제로 동작하는 과정(클릭, 입력 등) 확인을 통해 디버깅하기 좋음</td>
</tr>
</tbody></table>
<br>
<br>

<h2 id="정적-크롤링-예시jsoup">정적 크롤링 예시(jsoup)</h2>
<p>이제 OMD 프로젝트에서 개발했던 내용을 예시로 정적 크롤링 구현을 더 자세히 알아보자.</p>
<p>나는 네이버 상품 정보를 크롤링하는 것이 목적이었기 때문에 상품 상세 페이지에 들어가 크롤링을 하였다. 먼저 개발자 도구를 이용해 내가 가져오고 싶은 정보가 들어있는 클래스를 찾는다. <br>
<img src="https://velog.velcdn.com/images/su_under/post/4df834d4-8673-4a7b-b26b-906fbcd7e62c/image.png" style="margin-top: -10px;"></p>
<p>예를 들어 상품의 링크를 크롤링 하고 싶다면</p>
<ul>
<li><p>상품 링크가 들어있는 클래스는 <code>productList_mall_link__TrYxC linkAnchor</code> (띄어쓰기 되어 있으면 다른 클래스임) </p>
</li>
<li><p>&#39;productList_value__B_IxM&#39; 및 &#39;linkAnchor&#39; 클래스를 갖는 <code>&lt;a&gt;</code>요소 선택하여 안에 있는 요소들 가져오기</p>
<ul>
<li><code>Elements linkElements = doc.select(&quot;a.productList_mall_link__TrYxC.linkAnchor&quot;)</code></li>
</ul>
</li>
<li><p>요소들 중에 ‘href’ 값 가져오기</p>
<ul>
<li><code>String link = linkElement.attr(&quot;href&quot;);</code></li>
</ul>
</li>
</ul>
<p>이와 같은 방법으로 상품의 다른 정보도 가져올 수 있다. </p>
<br>

<h3 id="구현-코드">구현 코드</h3>
<p>추출한 데이터로 JSON 객체를 생성하여 결과를 출력하도록 하였다.</p>
<pre><code class="language-java">public static void main(String[] args) {

        try {

            String URL = &quot;https://search.shopping.naver.com/catalog/26101847522&quot;;

            Document doc = Jsoup.connect(URL).get();

            Elements priceElements = doc.select(&quot;a.productList_value__B_IxM.linkAnchor&quot;);
            Elements linkElements = doc.select(&quot;a.productList_mall_link__TrYxC.linkAnchor&quot;);
            Elements storeNameElements = doc.select(&quot;a.productList_mall_link__TrYxC.linkAnchor&quot;);

            // 데이터를 저장할 리스트 생성
            List&lt;JsonObject&gt; pages = new ArrayList&lt;&gt;();

            for (int i = 0; i &lt; linkElements.size(); i++) {
                Element priceElement = priceElements.get(i);
                Element linkElement = linkElements.get(i);
                Element storeNameElement = storeNameElements.get(i);

                // 각 요소로부터 데이터 추출
                Elements priceTag = priceElement.select(&quot;em&quot;);
                String priceText = priceTag.first().text();
                int price = Integer.parseInt(priceText.replaceAll(&quot;,&quot;, &quot;&quot;));
                String link = linkElement.attr(&quot;href&quot;);
                Elements imgTag = storeNameElement.select(&quot;img&quot;);
                String storeName = imgTag.attr(&quot;alt&quot;).isEmpty() ? storeNameElement.select(&quot;span&quot;).first().text() : imgTag.attr(&quot;alt&quot;);

                // 추출한 데이터로 JSON 객체 생성
                JsonObject page = new JsonObject();
                page.addProperty(&quot;price&quot;, price);
                page.addProperty(&quot;link&quot;, link);
                page.addProperty(&quot;storeName&quot;, storeName);

                // 리스트에 JSON 객체 추가
                pages.add(page);
            }

            // Gson 인스턴스 생성 및 포맷팅 설정
            Gson gson = new GsonBuilder().setPrettyPrinting().create();

            // 리스트를 JSON 형식으로 변환
            String jsonOutput = gson.toJson(pages);

            // 결과 출력
            System.out.println(jsonOutput);
        }

        catch (Exception e) {
            System.out.println(&quot;크롤링 실패 : &quot; + e);
        }
    }</code></pre>
<br>

<h3 id="최종-응답">최종 응답</h3>
<pre><code class="language-json">[
  {
    &quot;price&quot;: 999000,
    &quot;link&quot;: &quot;https://cr.shopping.naver.com/adcr.nhn?x\u003dk%2BzHLrjL4YXL8qDWiCmeT%2F%2F%2F%2Fw%3D%3DsYSO5S17xTdKFroTOyKK%2F8zu3qYkv6M99T52J5kP3%2Fll%2BowoAioOiY18LO0jYfDaZRqWjhDaoi6GqQa54Kob2J7DOR%2FLUjiefYuuPC23vP0EhhS9KWdbDew7L8h4moZBY0Sd0WzzbtHCCsXhw4SRIOHt%2Bn73F6F3JcLTBHczAxorQ1YZ3e2ivlUvLb3Xn9UN9gUsT12Bu8xqjL6SaemOd6iAey3v4wktBHdn6qRW7hkACFtWopB3rbp%2FPE7s8EJoBcikgMj90vcTwSpxO97bxXG9byOBGOfjXaFZo1KsHgeekCg4JeanhsEnZNo09%2BpY4Wv0MYkUn5C5w5sv8tH3%2B74zJPW%2FfGQnsXEGGy93Hpd654J8UO7LDGXdPU9Do8oyM%2ButNgbXZ3dsXpLc8YhINXNOEhemO5P9mLSQNZHUcpgINzKcybEmPJ9P6hlytdV%2FhA8vpR3VOt0APnKewTXr7mVylINQo7ElbqUbLjfRXqZ%2FCLfowEHUrDR9sAPaYWsG%2B13cDGb%2FrI2jqv2iJSp%2Bj9vtecQFyE2azzkNtPPJxVUBxcv7xPv1IUIA0oI%2FsyPanQpNz%2F5J39zgYSoJnoMFP7hCN%2Bk2BrhjdW1E5%2FiscKNm25nlgMyvZ%2BtcUupeeTdM7RQalJDjj8N2WnE9xXcPDQVLpCy%2BgP77opXdRQAW9vXlOiRhp6rL1Vmp6LQUc2QK8cg3Nd5g38NkedCyxjzy%2BOJ%2FyawiXHdfoit1OkZnYa6ck0tNx2sfNEdlwSQS7ZmndvnSe5oN739mtxlHRYC3cuA%3D%3D\u0026nvMid\u003d33760868983\u0026catId\u003d50000151&quot;,
    &quot;storeName&quot;: &quot;쿠팡&quot;
  },
  {
    &quot;price&quot;: 1064000,
    &quot;link&quot;: &quot;https://cr.shopping.naver.com/adcr.nhn?x\u003dH4nkNidr6y%2Ft1hH4JuzTPP%2F%2F%2Fw%3D%3DscNutFN3ggRNd2WcwFGar0JEJYj%2FCvNBT0nRILDms2Og7dYsxYR5eD7BddafDuUSxIT8fUh0betiHo2AgKod7YrDOR%2FLUjiefYuuPC23vP0EhhS9KWdbDew7L8h4moZBYxjuZuWah2sVOXCU6bylHeinJD2fi8TssCNCFWWTXcGByVwZJ7X67RIS84%2Bmq1agXcQD1noxS%2BFwck1nBaK49RFVd6Z%2BmZ5fvTALbBBPmfKKpJnalZl8YBNTVY%2F%2FSODJ0tjPf%2BkO8sKhS1WW9YdBV03yNYQltN87SjYG1iETcvaD5QUbj8j%2BgWub8moqkNk4QIYNQeQI%2FAKw7OLgJzexWCweMsZPpK61CBykFtbVtVy9Ia%2FIDcyjh15RQN5IM5zaTTGXPLtT04LXQ9V2JVHaeqnFTNKxWUq6U1dBjCG4mpVa1KEUJP%2Bo1hqOgls7QYVk0GLvQWxFx9uhCq%2BMcgWK7juN2FCQxeHAREnJLCjEC1%2Fnoq0pAGSlSx%2B%2FlpdPKUMerJVnN%2Bv%2FqwfkpIz2XakWUSmmuCxujaue8DTUK9e4xcXWsslyxaerMTcYkE%2Ba3Q229o2y147KkGAjDh%2FSQEO0VUZwOV9dp4S7QpwqwVXmEZq7fIwGzGVz23oVlJZpHgP8pYMpEvHVchTJK6LsvDw1iKj%2FglV9IbLJzhS8ns2hAPNcGGidsrWa0UJ7zfU5ghYz6\u0026nvMid\u003d45438540595\u0026catId\u003d50000151&quot;,
    &quot;storeName&quot;: &quot;Speed 마켓&quot;
  },
  {
    &quot;price&quot;: 1087000,
    &quot;link&quot;: &quot;https://cr.shopping.naver.com/adcr.nhn?x\u003dhyATL4EQTSCJI14141N93f%2F%2F%2Fw%3D%3DsRk%2B%2BD2iccSTtJrw9upUuxXSqJ1i3DtXeHbTFdceEh%2FKW6AjQiAZ%2FB4Af9g%2B0FCDNrJpJBj3kysEPAbCBRw86ArDOR%2FLUjiefYuuPC23vP0EhhS9KWdbDew7L8h4moZBYrFRN%2BLM1yJAr78kyjmQeuVavFoQZioHQNBzgV6NBWA6ue%2BYK%2Bd%2BRAmQWYHqELJz%2BvlSSixewNPJGNnWKKU2LLU%2BqWMaHFjcn2Qi%2BNoZq%2Fiik6v2w9jBqMGR4TTsgYaQnbqiQh4KIc1%2B7R9K%2FHxeQP8hxdv3Pmn%2FPwawpIB%2FZSgxMjxacOi%2BJsSCMNBjTIzFea2Xl1Fbag77Pjxwqc%2FE0phJBKTaSsuVYIRNVz8F7i%2Bye2%2B%2BLGasgn1SPx%2B%2BwsW8eqwbK%2BNLjghCocGW4o4m5sPtecQFyE2azzkNtPPJxVUCEDI9mDIyfOSmpdAIqfiafFW4lvPEf9y%2BNKlQBtqAztEifXjHmybBizFclN4U7JQah%2BTRygi%2F1r%2BY%2FGW8ylHG%2FuNMPomocnLCWty6Z6THBGkkn9yfqj5xl8K8PeLq5OoVXYu4v%2BmzgB82Clo5bspGghRPhTYqStO%2FqSsw7nlpfhGXzppE%2BIUXVbzcOFUNQ9XiDeOwjzor8VEyJ1g%2FUPWuIWqEZdn4U861r1ZnfzJAjK1gFlTKFPe21DLnCBqCm7M%2FixNmtY2aBcmzYIqGgGFzu%2FyZl20umiDjdF2swL7yzHw%3D%3D\u0026nvMid\u003d85455581054\u0026catId\u003d50000151&quot;,
    &quot;storeName&quot;: &quot;Two Step&quot;
  },
  {
    &quot;price&quot;: 1279200,
    &quot;link&quot;: &quot;https://cr.shopping.naver.com/adcr.nhn?x\u003dV%2Fhb006vYCMlW%2FabkkcpX%2F%2F%2F%2Fw%3D%3DsOhZZmfYSs45BVMmTOL1eoRshYkx9IQ5rzCD9ym2Ejp%2FRzB%2BoaVCyqiNkrjXYPYIXUHRkCnKbvI2VGdJSPWoNbag2IRyL9IrmhU2tJJCzkgvrOIflvl7ZVJx%2Bxl22SEnOvkAs6vR%2B6RlbGOBOw0blchnrG%2F9DJSAVy9GUUR%2FUh2ds5fWo0DNJ%2Fo0lzPbJYG1up8rTg8Hsbq4gWz4x6HP4C8qXj2vIkEKPE4VNYiU1zUE6iH6stKuG1q8n5%2BFw74kLp1t6%2B%2BXb8TKBKNrp%2BJA%2BjhsC3618HmsntmF01B7VaCAJjvnnruqQjYd4%2FSCfsTcl1tkq3amKHOOxk%2Fso7OOixWvqR2unsG9Pw6XBns%2FftjJo%2BG2TIH2qvnDlCxXH4V6E11PVT7hdN%2BFjcVZaNBq5mV0TOXEOkES0qEaxWvHD4Orredsg4mAua3L2XrbO9ZPFhokmdYwXPOcHE2bt%2BJizwCePcoyt3fweufMhlWz1upsG6Emsta1Dc2ParjzMoniGaDPJYjd8VmPnoCmTaJb%2B%2B3tf4Eep3C2HOrJb4i%2BpcSLjdhQkMXhwERJySwoxAtf5kV8l8Q20RegoAj1ynOcXdGmuCxujaue8DTUK9e4xcXVSCMZAHeM%2B9Cxsjjek7T7StW9Svb%2Fl4tf%2BaXc%2FaEKpNDfOqfqN3bF4pwcILRviipHOvQtRMXkC0o5j108LHj7OlTDDO3XIQ54%2FaIUF0h18HGqUOa4846iGybLWrhAw%2FsBWkbDrUFJ2hBG8QaB8WVzn\u0026nvMid\u003d40171968148\u0026catId\u003d50000151&quot;,
    &quot;storeName&quot;: &quot;알렛츠ALLETS&quot;
  },
  {
    &quot;price&quot;: 1291750,
    &quot;link&quot;: &quot;https://cr.shopping.naver.com/adcr.nhn?x\u003djjR9jRNHVWXaRoT70ZL9T%2F%2F%2F%2Fw%3D%3Ds6UBttSbYvTdyJniKkkUKZte8yZ%2FYWKWLprMnbNkCkOAv%2FnT8Uh8Ngqz%2BFJ5tGt3O26QwuBrhdKyhuAmGEtG4Uag2IRyL9IrmhU2tJJCzkgvrOIflvl7ZVJx%2Bxl22SEnOGc3LeEY%2FFI62jpahvFHll8NagcSl3iH%2FOEhTwotJzxfxjdACsIBxxVYyPYjSvLIWQztWcK4rmBg2xTDekdqZpZ5ksoO5NU26ylSFPtwZwwVTJbvG9iQ4gJvlFaowLRbX6WFubgOpif7sEVcUzG3kx6RkgKJIQj0bGtNP3KpKq2w1in%2FWb1uzJpgdCOr4mtlvP2tfKoQ0NQPyiad34AQ6K9uJWUq0D0CKT8syS3fRHyyqU8NjY8sy9fcMdU9KC8sZAiFWNgVY5moXKBEiMwqHkuWkDjJ0rQWcMkGw0eynwCoLAp2Zct%2FFfyybwQB4kT8h4m%2FJKzftBiGzZOyBIX3DuoTKrQ5QJw5qEXj4ZkG8FdIGWi6R00uV%2BDAgpVR2mQkMsgpOgtFy0FKDW1YUptRTnjw%2BwJTmqlroAQqM0X5DM%2FqcXVy%2BgvBJe1BCxMwYIjAzP7TTXD9xXm0EeGXraW6Vk17UT%2F08Dkot4Grp0h9tRi2j%2BJ0%2Bfg0sTsj2G8ZVQWkuiDTfUcGpRvY2mDclp%2FyW8v9DVojvnjaHdvmdrPNTU2iu6AW8wDhRtWP15WuAQ%2BubJXSFlseqJb%2BD9y1uadTE7g%3D%3D\u0026nvMid\u003d45923657468\u0026catId\u003d50000151&quot;,
    &quot;storeName&quot;: &quot;G마켓&quot;
  }
]</code></pre>
<br>

<h2 id="크롤링을-할-때-주의할-점">크롤링을 할 때 주의할 점</h2>
<p>크롤링을 하게 되면 반복적으로 페이지를 왔다갔다 하게 되는 경우가 있는데, 일련의 과정을 반복하게 되면 웹에서 크롤러라고 판단하여 서버를 차단하게 된다.</p>
<p>왜냐하면 우리의 눈에는 보이지않는 백엔드 영역에서는 서버로 많은 데이터와 패킷들이 지나다니고 있기 때문이다. 페이지를 왔다갔다하면 데이터가 반복적으로 지나다니는 과정에서 서버가 과부하가 되고, 터져버릴 수 있다. 이런 상황을 방지하기 위해 큰 기업들은 여러가지 함정을 설치한다.</p>
<p><img src="https://velog.velcdn.com/images/su_under/post/61bfaf74-f4d5-4ac3-b404-322b18786ae5/image.png" alt=""></p>
<p>이런식으로 에러가 뜨기 때문에 반복적인 행동을 하지 않도록 주의하자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP 메서드에 대해]]></title>
            <link>https://velog.io/@su_under/HTTP-%EB%A9%94%EC%84%9C%EB%93%9C%EC%97%90-%EB%8C%80%ED%95%B4</link>
            <guid>https://velog.io/@su_under/HTTP-%EB%A9%94%EC%84%9C%EB%93%9C%EC%97%90-%EB%8C%80%ED%95%B4</guid>
            <pubDate>Tue, 09 Apr 2024 07:22:25 GMT</pubDate>
            <description><![CDATA[<h2 id="get-post-put-delete❓">GET, POST, PUT, DELETE❓</h2>
<p>평소에 개발을 하면서 HTTP 메서드를 이용하는데, GET, POST, PUT, DELETE와 같은 메서드들을 <strong>내가 명확하게 사용하고 있는 걸까?</strong> 라는 궁금증이 생겼다. 이 궁금증과 동시에 <strong>HTTP 메서드라는 게 정확히 무엇이고, 왜 사용하는 것인지</strong> 근본적인 의문이 생겨 블로그로 작성해보고자 한다.</p>
<br>

<h2 id="🛠️-rest">🛠️ REST</h2>
<p><img src="https://velog.velcdn.com/images/su_under/post/731e2724-2415-4129-a091-2bed3762f08e/image.png" alt=""></p>
<p>HTTP 메서드에 대해서 알기 위해서, 먼저 정의되는 것이 REST이다.</p>
<h3 id="rest란">REST란?</h3>
<p><strong>REST(Representational State Transfer)</strong>의 약자로, 자원을 이름으로 구분하여 해당 자원의 상태를 주고받는 모든 것을 의미한다. REST는 다음과 같은 3가지로 구성이 되어있다.</p>
<ul>
<li><strong>자원(Resource) : HTTP URI</strong></li>
<li><strong>자원에 대한 행위(Verb) : HTTP Method</strong></li>
<li><strong>자원에 대한 행위의 내용 (Representations) : HTTP Message Pay Load</strong></li>
</ul>
<p>여기서 글의 주제인 HTTP 메서드가 등장한다. 즉, HTTP 메서드는 REST를 지키면서 행위를 전달하는 방법이다.</p>
<br>

<h2 id="http-메서드">HTTP 메서드</h2>
<p>더 자세하게 HTTP 메서드에 대해 살펴보자.</p>
<h3 id="http-메서드-종류">HTTP 메서드 종류</h3>
<p>앞에서 말한 것처럼 HTTP 메서드는 행위를 전달하는 방법인데, 이 방법에는 여러가지 종류가 있다. 총 9가지의 HTTP 메서드가 있고 이 중 주로 쓰이는 메서드는 5가지이다.</p>
<ul>
<li><strong>주요 메서드</strong><ul>
<li><strong>GET</strong> : 리소스 조회</li>
<li><strong>POST</strong> : 요청 데이터 처리, 주로 등록에 사용</li>
<li><strong>PUT</strong> : 리소스를 대체(덮어쓰기), 해당 리소스가 없으면 생성</li>
<li><strong>PATCH</strong> : 리소스를 부분 변경 (PUT이 전체 변경, PATCH는 일부 변경)</li>
<li><strong>DELETE</strong> : 리소스 삭제</li>
</ul>
</li>
<li><strong>기타 메서드</strong><ul>
<li><strong>HEAD</strong> : GET과 동일하지만 메시지 부분(body 부분)을 제외하고 상태 줄과 헤더만 반환</li>
<li><strong>OPTIONS</strong> : 대상 리소스에 대한 통신 가능 옵션(메서드)를 설명</li>
<li><strong>CONNECT</strong> : 대상 자원으로 식별되는 서버에 대한 터널을 설정</li>
<li><strong>TRACE</strong> : 대상 리소스에 대한 경로를 따라 메시지 루프백 테스트를 수행. 현재 정보 유출 위험이 있어 거의 모든 브라우저가 지원 안함</li>
</ul>
</li>
</ul>
<h3 id="1-get">1. GET</h3>
<p>GET 메서드는 <strong>데이터를 읽거나 검색</strong>할 때 사용되는 메서드이다. 요청이 성공적으로 이루어진다면 200(Ok) HTTP 응답 코드를 리턴한다. 에러가 발생하면 주로 404(Not found) 에러나 400(Bad request) 에러가 발생한다.</p>
<ul>
<li>HTTP 명세에 의하면 GET 요청은 오로지 데이터를 읽을 때만 사용되고 수정할 때는 사용하지 않는다.</li>
<li>GET 요청은 idempotent 하다.</li>
<li>같은 요청을 여러 번 하더라도 변함없이 항상 같은 응답을 받을 수 있다.</li>
<li>데이터를 변경하는 연산에 사용하면 안된다.</li>
</ul>
<p><strong>예시</strong></p>
<pre><code>GET /user/1</code></pre><p>데이터를 조회하는 것이기 때문에 요청시에 Body 값과 Content-Type이 비워져있다. 조회할 데이터에 대한 정보는 URL을 통해서 파라미터를 받고 있는 모습을 볼 수 있다.</p>
<p>데이터 조회에 성공한다면 Body 값에 데이터 값을 저장하여 성공 응답을 보낸다.</p>
<p>GET은 캐싱이 가능하여 같은 데이터를 한번 더 조회할 경우에 저장한 값을 사용하여 조회 속도가 빨라진다.</p>
<h3 id="2-post">2. POST</h3>
<p>POST 메서드는 <strong>새로운 리소스를 생성</strong>할 때 사용된다. 성공적으로 요청을 완료하면 201(Created) HTTP 응답을 반환한다.</p>
<ul>
<li>POST 요청은 idempotent 하지 않다.</li>
<li>같은 POST 요청을 반복해서 했을 때 항상 같은 결과물이 나오는 것을 보장하지 않는다.</li>
</ul>
<p><strong>예시</strong></p>
<pre><code>POST /user
body : {date : &quot;example&quot;}
Content-Type : &quot;application/json&quot;</code></pre><p>데이터를 생성하는 것이기 때문에 요청시에 Body 값과 Content-Type 값을 작성해야한다. 해당 예시는 JSON을 통해서 작성된 예시이다.</p>
<p>URL을 통해서 데이터를 받지 않고, Body 값을 통해서 받는다.</p>
<p>데이터 조회에 성공한다면 Body 값에 저장한 데이터 값을 저장하여 성공 응답을 보낸다.</p>
<h3 id="3-put">3. PUT</h3>
<p>PUT 메서드는 <strong>리소스가 있으면 업데이트하고 없으면 생성</strong>하기 위해 서버로 데이터를 보내는 데 사용된다.</p>
<ul>
<li>PUT 요청은 idempotent 하다.</li>
<li>동일한 PUT 요청을 여러 번 호출하면 항상 동일한 결과가 생성된다.</li>
</ul>
<p><strong>예시</strong></p>
<pre><code>PUT /user/1
body : {date : &quot;update example&quot;}
Content-Type : &quot;application/json&quot;</code></pre><p>데이터를 수정하는 것이기 때문에 요청시에 Body 값과 Content-Type 값을 작성해야한다. 해당 예시는 JSON을 통해서 작성된 예시이다.</p>
<p>URL을 통해서 어떠한 데이터를 수정할지 파라미터를 받는다. 그리고 수정할 데이터 값을 Body 값을 통해서 받는다.</p>
<p>데이터 조회에 성공한다면 Body 값에 저장한 데이터 값을 저장하여 성공 응답을 보낸다.</p>
<h3 id="4-delete">4. DELETE</h3>
<p>DELETE 메서드는 지정된 리소스를 <strong>삭제</strong>한다.</p>
<h3 id="예시">예시</h3>
<pre><code>DELETE /user/1</code></pre><p>데이터를 삭제하는 것이기 때문에 요청시에 Body 값과 Content-Type 값이 비워져있다.</p>
<p>URL을 통해서 어떠한 데이터를 삭제할지 파라미터를 받는다.</p>
<p>데이터 삭제에 성공한다면 Body 값 없이 성공 응답만 보내게 된다.</p>
<h3 id="http-메서드의-속성">HTTP 메서드의 속성</h3>
<p>위에서  idempotent 라는 단어가 반복적으로 나왔는데 이것은 HTTP 메서드의 속성이다. 대체 idempotent 하다는 것이 무엇인지 HTTP 메서드의 다른 속성들과 함께 알아보자.</p>
<p>HTTP 메서드는 속성에는 다음과 같이 3가지 속성이 있고 해당 속성별로 메서드를 구분할 수 있다.</p>
<ul>
<li><p><strong>안전함(Safe Methods)</strong></p>
<p>  이 말은 계속해서 메서드를 호출해도 리소스를 변경하지 않는다는 뜻이다. 주요 메서드 중에는 GET 메서드가 안전하다고 볼 수 있다.</p>
</li>
<li><p><strong>멱등성(Idempotent Methods)</strong></p>
<p>  이 말은 메서드를 계속 호출해도 결과가 똑같다는 뜻이다. GET, PUT, DELETE는 멱등하다고 볼 수 있지만 POST나 PATCH는 멱등하다고 볼 수 없다.</p>
<p>  PATCH가 왜 멱등하지 않다고 하는건지는 <a href="https://oen-blog.tistory.com/211">여기</a>를 참고하자.</p>
</li>
<li><p><strong>캐시 가능(Cacheable Methods)</strong></p>
<p>  캐시 가능하다는 말은 말 그대로 <a href="https://readinggeneral.tistory.com/entry/%EC%BA%90%EC%8B%9CCache%EC%99%80-%EC%BA%90%EC%8B%B1Caching-%EC%A0%95%EB%A6%AC-from-10%EB%B6%84-%ED%85%8C%ED%81%AC%ED%86%A1">캐싱</a>을 해서 데이터를 효율적으로 가져올 수 있다는 뜻이다. GET, HEAD, POST, PATCH가 캐시가 가능하지만 실제로는 GET과 HEAD만 주로 캐싱이 쓰인다고 한다.</p>
</li>
</ul>
<br>


<h2 id="🔗-참고-자료">🔗 참고 자료</h2>
<ul>
<li><a href="https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-HTTP-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A2%85%EB%A5%98-%ED%86%B5%EC%8B%A0-%EA%B3%BC%EC%A0%95-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC">https://inpa.tistory.com/entry/WEB-🌐-HTTP-메서드-종류-통신-과정-💯-총정리</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Methods">https://developer.mozilla.org/ko/docs/Web/HTTP/Methods</a></li>
<li><a href="https://velog.io/@devdam/HTTP">https://velog.io/@devdam/HTTP</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Grafana와 Slack 연동하여 Alert 설정하기]]></title>
            <link>https://velog.io/@su_under/Grafana%EC%99%80-Slack-%EC%97%B0%EB%8F%99%ED%95%98%EC%97%AC-Alert-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@su_under/Grafana%EC%99%80-Slack-%EC%97%B0%EB%8F%99%ED%95%98%EC%97%AC-Alert-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 22 Jan 2024 17:50:32 GMT</pubDate>
            <description><![CDATA[<p>오늘은 Grafana와 Slack을 연동하여 알림을 설정하는 방법에 대해 알아보자. 알림 설정을 해두면 시스템의 상태를 더 효율적으로 모니터링 할 수 있다.</p>
<h1 id="slack-설정하기">Slack 설정하기</h1>
<p>Grafana가 Slack에 알림을 보내기 위해서는 Webhook URL을 생성해야 하는데, 그 과정에 대해 설명해보겠다.</p>
<h2 id="1-app-생성">1. App 생성</h2>
<p>먼저, Slack API 홈페이지(<a href="https://api.slack.com/apps">https://api.slack.com/apps</a>)에 들어가보자.
아래와 같은 화면에서 Create New App을 클릭한다.
<img src="https://velog.velcdn.com/images/su_under/post/368c6902-1eea-4df0-9913-f7b2db4d82ea/image.png" alt="">
<br/>
From Scratch를 누르고 App Name과 workspace를 설정해준다.
<img src="https://velog.velcdn.com/images/su_under/post/23a0e4af-c4be-4a92-9c34-ccb660ded449/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/19883df6-6482-4086-9281-78c83ec8eddd/image.png" alt="">
<br/>
다음과 같이 App이 생성되었다. 이 App을 클릭하면 왼쪽에 설정 메뉴들이 보인다
<img src="https://velog.velcdn.com/images/su_under/post/6f7a605e-501c-49f8-af28-c98b00cbbab0/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/8f305fd8-992c-486a-b846-ec54643d3097/image.png" alt="">
<br/>
만약 App의 사진을 등록하고 싶다면 화면을 쭉 내려서 Display Information에서 사진을 등록해주면 된다.
<img src="https://velog.velcdn.com/images/su_under/post/8693e52c-ab99-42bd-8424-260ce682c6d6/image.png" alt="">
<br/></p>
<h2 id="2-token-생성">2. Token 생성</h2>
<p>왼쪽의 메뉴에서 OAuth &amp; Permissions를 클릭하면 다양한 옵션들을 설정할 수 있는 화면이 나온다.
<img src="https://velog.velcdn.com/images/su_under/post/a8ca0ee0-dfb9-4be1-aecc-6304dc0f4eb0/image.png" alt="">
<br/>
화면을 밑으로 내리다보면 Scopes가 나오고, Grafana에서 Slack API를 통해 메시지를 전송해야 하기 때문에 chat:write를 추가해준다.
<img src="https://velog.velcdn.com/images/su_under/post/67180b88-51d1-4834-a092-71c1dbeb5cbc/image.png" alt="">
<br/>
다시 위로 올라가서 OAuth Tokens for Your Workspace에서 Install to Workspace를 클릭하고 허용을 눌러 Token을 생성해준다.
<img src="https://velog.velcdn.com/images/su_under/post/e53dc0a1-047b-4761-96be-1a2471702039/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/1d6f8300-3919-4901-a316-3eb3297f6c6c/image.png" alt="">
<br/></p>
<h2 id="3-webhook-url-생성">3. Webhook URL 생성</h2>
<p>왼쪽의 메뉴에서 Incoming Webhooks을 클릭. Incoming Webhooks 활성화
<img src="https://velog.velcdn.com/images/su_under/post/f1b94b6c-f264-4a87-89a1-08066675a283/image.png" alt="">
<br/>
아래의 Webhook URLs for Your Workspace에서 Add New Webhook to Workspace 클릭. 알림 받고 싶은 채널 설정
<img src="https://velog.velcdn.com/images/su_under/post/0cdf8432-1852-4459-9158-ee186f1050f9/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/ba0c40a4-4376-4db8-a627-8e31d38a5540/image.png" alt="">
<br/>
다음과 같이 Webhook URL이 생성된 것을 볼 수 있다.
<img src="https://velog.velcdn.com/images/su_under/post/bffefae8-ea9d-4d75-be0e-54159055f5ba/image.png" alt="">
<br/></p>
<h2 id="4-slack에서-채널-추가">4. Slack에서 채널 추가</h2>
<p>Slack앱에 들어가서 알림을 받고 싶은 채널에 들어간 다음, @APP_NAME을 입력한다. 그러면 자동으로 Grafana앱이 추가된다.
<img src="https://velog.velcdn.com/images/su_under/post/a50967a7-6e18-49a6-a0e7-4ec062d9d4a0/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/85329b49-26d3-43ff-a765-24ec77aa6124/image.png" alt="">
<br/></p>
<h1 id="grafana-설정하기">Grafana 설정하기</h1>
<h2 id="1-alert-rule-생성">1. Alert Rule 생성</h2>
<p>Grafana에 접속하여 메뉴에서 Alert rules을 클릭 -&gt; New alert rule클릭
<img src="https://velog.velcdn.com/images/su_under/post/a9cda5d3-e787-49b2-97b4-a0257032ae71/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/2785faf3-3d90-4d21-91a1-a74943ad2b4b/image.png" alt="">
<br/>
rule name과 query를 알맞게 작성하고 Folder와 Group을 만들어 설정해준다.
<img src="https://velog.velcdn.com/images/su_under/post/385c6885-2ca5-46da-97dd-65fcfc075b23/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/bfe56d89-5134-435f-a097-fcc41459eea9/image.png" alt="">
<br/></p>
<h2 id="2-contact-point-생성">2. Contact point 생성</h2>
<p>alert rule을 생성하였다면, 다음은 내가 알림을 전달받고 싶은 수단과 Grafana를 연결해야 한다.
Contact points에 들어가서 Add contact point 클릭
<img src="https://velog.velcdn.com/images/su_under/post/bdab5af9-0ed5-4e70-ad33-3c6b6e0de4a6/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/b23c9ca9-8ce0-4e5d-8d17-8abbb7243049/image.png" alt="">
<br/>
Name에 내가 원하는 이름을 입력하고 Integration에는 내가 알림을 받고 싶은 수단을 선택한다. 나는 Slack과 연결하는 것이 목적이므로 Slack을 선택하고 아까 생성했던 Webhook URL을 복사하여 넣어준다.
<img src="https://velog.velcdn.com/images/su_under/post/51276bfc-bc06-414d-acad-f955562c9b60/image.png" alt="">
<br/>
이제 잘 연결되었는지 확인하기 위해 Test 해보자. Test를 클릭하고 Send test notification을 클릭하면 Slack 메시지가 전송된다.
<img src="https://velog.velcdn.com/images/su_under/post/be066256-7a2b-4b19-abc9-c06883a72c13/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/aa7b5cbd-a370-48f2-a447-22fae2593520/image.png" alt="">
<br/>
아래와 같이 메시지가 전송된 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/su_under/post/43a4007b-7909-433b-aa4b-e416ca236537/image.png" alt="">
<br/></p>
<h2 id="3-notification-policy-생성">3. Notification policy 생성</h2>
<p>생성된 contact point를 notification policy에 등록해주지 않으면 alert가 발생해도 alert 메시지가 전송되지 않기 때문에 꼭 등록을 해주어야 한다.
메뉴에서 Notification policies 클릭. Edit를 눌러 Deflaut contact point에 아까 설정한 contact point의 name을 입력한다.
<img src="https://velog.velcdn.com/images/su_under/post/693a0590-f36c-44c6-8fb1-b8824ad8b0e8/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/ee5025bc-3e93-4e2a-85e7-042e63faa2d4/image.png" alt=""></p>
<p>이제 Grafana에 alert가 발생하면 Slack에 메시지가 정상적으로 전달될 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker와 cAdvisor 연동하기]]></title>
            <link>https://velog.io/@su_under/Docker%EC%99%80-cAdvisor-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@su_under/Docker%EC%99%80-cAdvisor-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Jan 2024 13:21:01 GMT</pubDate>
            <description><![CDATA[<h2 id="✨-cadvisor">✨ cAdvisor</h2>
<blockquote>
<p>cAdvisor는 Google에서 개발한 오픈 소스 컨테이너 리소스 사용 및 성능 분석 도구이다. &quot;Container Advisor&quot;의 약자로, 주로 Docker 및 Kubernetes와 같은 컨테이너 오케스트레이션 환경에서 사용된다.  컨테이너 환경에서 작동하는 각각의 컨테이너에 대한 리소스 사용 및 성능 데이터를 수집하고 제공하여 시스템 관리자나 개발자가 애플리케이션 및 컨테이너의 동작을 모니터링할 수 있게 해준다.</p>
</blockquote>
<br>

<h2 id="🤷♀️-prometheus-cadvisor">🤷‍♀️ Prometheus? cAdvisor?</h2>
<p>그렇다면 Prometheus와 cAdvisor는 뭐가 다른걸까?</p>
<p>Prometheus와 cAdvisor는 모두 컨테이너 및 마이크로서비스 환경에서 사용되는 모니터링 도구이지만, 몇 가지 중요한 차이점이 있다.</p>
<p>먼저, Prometheus는 분산된 시스템 및 서비스 아키텍처에서 발생하는 여러 구성 요소의 모니터링과 경고에 중점을 둔다. 여러 서비스 및 시스템의 지표를 수집하고 중앙 집중식으로 저장하여 통합된 모니터링을 제공한다. 반면, cAdvisor는 개별 컨테이너에 대한 리소스 사용 및 성능 데이터를 제공한다는 점이 가장 큰 차이점이다.</p>
<p>또한 데이터 수집 방식에 차이가 있다. Prometheus는 내장된 데이터 저장소를 사용하여 지표를 수집하고 저장한다. 데이터베이스에 데이터를 저장하고 쿼리할 수 있다. 또한 지원되는 다양한 수집기를 사용하여 여러 시스템과 통합할 수 있다. cAdvisor는 호스트 및 컨테이너 레벨에서 수집된 데이터를 프로메테우스와 같은 저장소에 직접 저장하지 않는다. 일반적으로는 각각의 노드에서 cAdvisor 인스턴스가 실행되며, 그 데이터는 외부 시스템에서 수집하거나 직접 cAdvisor 웹 UI를 통해 확인할 수 있다.</p>
<br/>

<hr>
<h1 id="docker와-cadvisor-연동하기">Docker와 cAdvisor 연동하기</h1>
<h3 id="1-docker-composeyml-파일-작성">1. docker-compose.yml 파일 작성</h3>
<pre><code class="language-yaml">version: &quot;3&quot;

services:
    prometheus:
        image: prom/prometheus
        container_name: prometheus
        volumes:
          - ./prometheus/config:/etc/prometheus
          - prometheus-data:/prometheus
        ports:
          - 9090:9090
        command:
          - &quot;--storage.tsdb.path=/prometheus&quot;
          - &quot;--config.file=/etc/prometheus/prometheus.yml&quot;
        restart: always
        networks:
          - t4y

    cadvisor:
        image: gcr.io/cadvisor/cadvisor

        container_name: cadvisor
        ports:
          - 8080:8080
        volumes:
          - /:/rootfs:ro
          - /var/run:/var/run:rw
          - /sys:/sys:ro
          - /var/lib/docker/:/var/lib/docker:ro
          - /dev/disk/:/dev/disk:ro
        networks:
          - t4y

networks:
  t4y:
    driver: bridge</code></pre>
<br>

<h3 id="2-prometheusyml-파일-작성">2. prometheus.yml 파일 작성</h3>
<pre><code class="language-yaml">global:
  scrape_interval: 15s
  scrape_timeout: 15s
  evaluation_interval: 2m

  external_labels:
    monitor: &#39;codelab-monitor&#39;
    query_log_file: query_log_file.log

scrape_configs:
  - job_name: &#39;monitoring-item&#39;
    scrape_interval: 10s
    scrape_timeout: 10s
    metrics_path: &#39;/metrics&#39;
    honor_labels: false
    honor_timestamps: false
    scheme: &#39;http&#39;

    static_configs:
      - targets: [&#39;prometheus:9090&#39;, &#39;cadvisor:8080&#39;]
        labels:
          service: &#39;monitor&#39;</code></pre>
<br>

<h3 id="3-컨테이너-실행">3. 컨테이너 실행</h3>
<p>명령어 <code>docker compose up -d</code> 로 도커 컨테이너를 실행한다.</p>
<p>그러면 다음과 같이 cadvisor 컨테이너가 추가된 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/su_under/post/8824150e-76b2-4599-9a2f-64166793983e/image.png" alt=""></p>
<br>

<p>cadvisor의 서버로 들어가면 다음과 같은 지표들을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/su_under/post/a96896d3-603d-48d6-83b8-ed464fbf83ef/image.png" alt=""> <img src="https://velog.velcdn.com/images/su_under/post/b4e5643d-ce89-42d2-a90f-42049c671d3e/image.png" alt=""></p>
<br>

<h2 id="❗️-cannot-detect-current-cgroup-on-cgroup-v2-에러">❗️ Cannot detect current cgroup on cgroup v2 에러</h2>
<p>만약 컨테이너를 실행했는데 cadvisor 컨테이너에서 <code>Cannot detect current cgroup on cgroup v2</code> 에러가 발생한다면 호환성 이슈일 가능성이 높다. 나도 같은 이슈가 발생하였는데, M1 칩의 명령어 set과 cAdvisor가 체크할 수 있는 명령어 set의 차이로 인해 이슈가 생기는 듯 하였다.</p>
<p>cAdvisor v0.45.0 버전부터는 arm 아키텍쳐를 포함한 여러 아키텍쳐를 지원하기 때문에 cAdvisor의 이미지 버전을 v0.45.0 이상으로 올리면 오류가 해결될 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker와 Prometheus, Grafana 연동하기]]></title>
            <link>https://velog.io/@su_under/Docker%EC%99%80-Prometheus-Grafana-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@su_under/Docker%EC%99%80-Prometheus-Grafana-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Jan 2024 12:37:09 GMT</pubDate>
            <description><![CDATA[<h2 id="✨prometheus">✨Prometheus</h2>
<blockquote>
<p>Prometheus는 메트릭 수집, 시각화, 알림, 서비스 디스커버리 기능을 모두 가지고 있는  오픈소스 기반의 모니터링 시스템이다. 처음에는 SoundCloud에서 만들어졌으나 2016년에는 쿠버네티스에 이어 두 번째로 CNCF(Cloud Native Computing Foundation) 산하 프로젝트 멤버로 들어가게 됐다.</p>
</blockquote>
<h2 id="✨grafana">✨Grafana</h2>
<blockquote>
<p>Grafana는 애플리케이션에서 수집된 지표, 로그 및 추적을 시각화하기 위한 오픈 소스 관찰 가능성 플랫폼이다. Torkel Ödegaard의 주도로 2014년 처음 릴리스되었으며, Prometheus, InfluxDB, ElasticSearch 및 기존 관계형 데이터베이스 엔진과 같은 다양한 데이터 소스에 연결된다.</p>
</blockquote>
<h2 id="✨node-exporter">✨Node Exporter</h2>
<blockquote>
<p>Prometheus Node Exporter는 하드웨어의 상태와 커널 관련 메트릭을 수집하는 메트릭 수집기이다. Prometheus는 Node Exporter의 metrics HTTP endpoint에 접근하여 해당 메트릭을 수집할 수 있다. Node Exporter로 부터 수집한 메트릭을 Prometheus내의 TSDB에 저장하여 PromQL로 메트릭을 쿼리해 서버 상태를 모니터링할 수 있다.</p>
</blockquote>
<br/>

<hr>
<h1 id="docker에-prometheus-grafana-연동하기">Docker에 Prometheus, Grafana 연동하기</h1>
<h3 id="1-docker-composeyml-파일-작성">1. docker-compose.yml 파일 작성</h3>
<pre><code class="language-yaml">version: &quot;3&quot;

networks:
  t4y:
    driver: bridge

  prometheus:
    image: prom/prometheus
    container_name: prometheus
    volumes:
      - ./prometheus/config:/etc/prometheus
      - prometheus-data:/prometheus
    ports:
      - 9090:9090
    command:
      - &#39;--storage.tsdb.path=/prometheus&#39;
      - &#39;--config.file=/etc/prometheus/prometheus.yml&#39;
    restart: always
    networks:
      - t4y

  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - 3000:3000
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning/:/etc/grafana/provisioning/
    restart: always
    depends_on:
      - prometheus
    networks:
      - t4y

  node_exporter:
    image: prom/node-exporter
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - &#39;--path.procfs=/host/proc&#39;
      - &#39;--path.rootfs=/rootfs&#39;
      - &#39;--path.sysfs=/host/sys&#39;
      - &#39;--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)&#39;
    ports:
      - &quot;9100:9100&quot;
    networks:
      - t4y

volumes:
  grafana-data:
  prometheus-data:</code></pre>
<h3 id="2-prometheusyml-파일-작성">2. prometheus.yml 파일 작성</h3>
<pre><code class="language-yaml">global:
  scrape_interval: 15s
  scrape_timeout: 15s
  evaluation_interval: 2m

  external_labels:
    monitor: &#39;codelab-monitor&#39;
    query_log_file: query_log_file.log

scrape_configs:
  - job_name: &#39;monitoring-item&#39;
    scrape_interval: 10s
    scrape_timeout: 10s
    metrics_path: &#39;/metrics&#39;
    scheme: &#39;http&#39;

    static_configs:
      - targets: [&#39;prometheus:9090&#39;, &#39;node_exporter:9100&#39;]
        labels:
          service: &#39;monitor&#39;</code></pre>
<h3 id="3-컨테이너-실행">3. 컨테이너 실행</h3>
<p>명령어 <code>docker compose up -d</code> 로 도커 컨테이너를 실행한다.</p>
<p>그러면 다음과 같이 prometheus, grafana, node_exporter 컨테이너가 추가된 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/su_under/post/8ac8de1e-f5be-4fdd-87b6-cd07320e19cb/image.png" alt=""></p>
<p>prometheus의 서버로 들어가서 Targets를 클릭하여 잘 연동되었는지 확인해보자.
<img src="https://velog.velcdn.com/images/su_under/post/23ca4c96-ea41-4dfc-980d-0424bfbd4eac/image.png" alt=""></p>
<p>다음과 같이 State가 모두 UP이므로 문제없이 연동되었다.
<img src="https://velog.velcdn.com/images/su_under/post/f8d9a3f6-9336-4792-bb59-0c58907c7dfd/image.png" alt=""></p>
<h3 id="4-데이터-소스-추가하기">4. 데이터 소스 추가하기</h3>
<p>Grafana 서버로 접속하여 Add new connection 클릭
<img src="https://velog.velcdn.com/images/su_under/post/d99eb179-86b9-4d32-aba4-fa19bd2d420c/image.png" alt=""></p>
<p>prometheus 검색, 데이터 소스 추가
<img src="https://velog.velcdn.com/images/su_under/post/0a2909aa-ea98-4ceb-9776-5b29a168c134/image.png" alt=""></p>
<p>Prometheus server URL에 <a href="http://prometheus:9090">http://prometheus:9090</a> 입력. Save&amp;test를 눌러 저장
<img src="https://velog.velcdn.com/images/su_under/post/86107771-3241-477a-aa95-d31c46e01cd2/image.png" alt=""><img src="https://velog.velcdn.com/images/su_under/post/9ed1be8a-6ffc-4f4c-9825-91e92fd4a0c3/image.png" alt=""></p>
<h3 id="5-대시보드-생성">5. 대시보드 생성</h3>
<p>대시보드를 아예 새로 만드는 것이 아니라 템플릿을 이용하여 좀 더 간단하게 대시보드를 생성하겠다.
Dashboards에 들어가서 Import클릭
<img src="https://velog.velcdn.com/images/su_under/post/aff133d5-779b-4261-9060-110350c790de/image.png" alt=""></p>
<p>템플릿의 Id에 1860을 입력 하고 Load 클릭. 만약 다른 템플릿을 적용하고 싶다면 <a href="https://grafana.com/grafana/dashboards/">https://grafana.com/grafana/dashboards/</a> 사이트에 접속하여 원하는 템플릿의 Id를 복사하면 된다.
<img src="https://velog.velcdn.com/images/su_under/post/c7b02ca4-4f62-40c2-9da6-2a7079a19984/image.png" alt=""></p>
<p>다음과 같이 데이터 소스를 prometheus로 설정하고 Import한다.
<img src="https://velog.velcdn.com/images/su_under/post/bdf5ba95-ec3d-4b57-ac06-999d17d0abe8/image.png" alt=""></p>
<p>아래와 같이 대시보드가 완성되었다! 이제 Node Exporter를 통해 서버의 메모리, CPU 사용량, Network Traffic 등을 모니터링 할 수 있다.
<img src="https://velog.velcdn.com/images/su_under/post/c680ce00-9970-43d1-8cbc-dd8bd3648388/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>