<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>noh_level0.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 28 Mar 2025 16:48:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. noh_level0.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/noh_level0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Elasticsearch] Docker에 Elasticsearch, Kibana, Logstash 설치하기]]></title>
            <link>https://velog.io/@noh_level0/Elasticsearch-Docker%EC%97%90-Elasticsearch-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@noh_level0/Elasticsearch-Docker%EC%97%90-Elasticsearch-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 28 Mar 2025 16:48:49 GMT</pubDate>
            <description><![CDATA[<h1 id="elasticsearch-설치가-필요한-이유">Elasticsearch 설치가 필요한 이유</h1>
<p>Elasticsearch는 &quot;검색 엔진&quot; 역할을 하는 <strong>외부 서버</strong>이다.
따라서 내 애플리케이션이 Elasticsearch 서버의 네트워크로 접속해서 데이터를 주고받아야 하므로 Elasticsearch 사용하려면 설치가 필요하다.</p>
<h1 id="몇-버전의-elasticsearch를-설치할까">몇 버전의 Elasticsearch를 설치할까?</h1>
<pre><code class="language-java">dependencies {
    ...
    implementation &#39;org.springframework.boot:spring-boot-starter-data-elasticsearch&#39;
}</code></pre>
<p>일단 build.gradle의 dependencies에 Spring Boot 버전에 맞는 Elasticsearch 라이브러리를 가져오는 코드를 추가해보았다.
<img src="https://velog.velcdn.com/images/noh_level0/post/d4e6644b-521f-44d5-8224-68a1a6955d7b/image.png" width="50%" height="50%">
버전 확인 결과 Spring Boot v3.3.1은 elasticsearch 8.13.4를 가져오고 있었다. 따라서 이에 맞춰 Elasticsearch 8.13.4 버전을 설치하였다.
<a href="https://docs.spring.io/spring-data/elasticsearch/reference/elasticsearch/versions.html">참고(Spring Data와 호환되는 Elasticsearch 버전)</a></p>
<h1 id="설치-시작">설치 시작</h1>
<h2 id="1-elsyml-및-dockerfile-작성">1. els.yml 및 Dockerfile 작성</h2>
<p>els.yml 을 작성하여 Elasticsearch만이 아니라, Elasticsearch + Kibana + Logstash까지 포함한 전체 ELK 스택을 한번에 실행하기 위한 구성을 해준다. 이를 통해 각각의 서비스를 따로 실행하고 연결 설정하는 번거로움을 줄일 수 있다.
즉, docker-compose를 만든 후 yml 파일을 읽어서 거기에 정의된 서비스들을 하나의 앱처럼 실행한다. 그렇게 된다면 docker-compose up 같은 명령어로 여러 컨테이너를 동시에 띄우는 게 가능해진다.</p>
<ul>
<li><p>els.yml</p>
<pre><code class="language-shell">version: &#39;3.7&#39;
services:
es:
  #image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
  build:
    context: .
    args:
      VERSION: 8.13.4
  container_name: es
  environment:
    - node.name=single-node
    - cluster.name=backtony
    - discovery.type=single-node
    - xpack.security.enabled=false
  ports:
    - 9200:9200
    - 9300:9300
  networks:
    - es-bridge

kibana:
  container_name: kibana
  image: docker.elastic.co/kibana/kibana:8.13.4
  environment:
    SERVER_NAME: kibana
    ELASTICSEARCH_HOSTS: http://es:9200
    ELASTICSEARCH_SSL_VERIFICATIONMODE: none
  ports:
    - 5601:5601
  # Elasticsearch Start Dependency
  depends_on:
    - es
  networks:
    - es-bridge

logstash:
  image: docker.elastic.co/logstash/logstash:8.13.4
  container_name: logstash
  environment:
    - ELASTICSEARCH_HOSTS=http://es:9200
    - MYSQL_USER=${MYSQL_USER}
    - MYSQL_PASSWORD=${MYSQL_PASSWORD}
  volumes:
    - ./logstash/config/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml
    - ./logstash/mysql-connector-j-8.3.0.jar:/usr/share/logstash/mysql-connector-j-8.3.0.jar
  depends_on:
    - es
  networks:
    - es-bridge
</code></pre>
</li>
</ul>
<p>networks:
  es-bridge:
    driver: bridge</p>
<pre><code>
- Dockerfile
```shell
# Dockerfile
ARG VERSION
FROM docker.elastic.co/elasticsearch/elasticsearch:${VERSION}
RUN elasticsearch-plugin install analysis-nori</code></pre><p>한국어는 nori 형태소 분석기를 사용해 검색할 것이므로 이를 함께 설치한다.</p>
<h3 id="dockerfile이-왜-필요할까">Dockerfile이 왜 필요할까?</h3>
<pre><code class="language-shell">es:
  #image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
  build:
    context: .
    args:
      VERSION: 8.13.4</code></pre>
<p>es 서비스는 위와 같이 구성되어 있다. 즉, image:를 직접 사용하는 대신 build 지시어로 현재 디렉토리에 있는 Dockerfile을 빌드해서 컨테이너를 생성하겠다는 의미이므로 Dockerfile이 필요해진다.</p>
<h2 id="2-elsyml에-정의된-내용대로-docker-compose-명령을-실행">2. els.yml에 정의된 내용대로 docker compose 명령을 실행</h2>
<pre><code class="language-shell">docker compose -f els.yml up -d</code></pre>
<p>els.yml이 있는 경로에서 cmd 창을 열고 위의 명령어를 실행한다. 이 명령어의 의미는 els.yml이라는 설정 파일을 사용하여 이 안에 정의된 컨테이너들을 빌드하고 실행하라는 의미가된다.
-d는 백그라운드에서 실행하라는 뜻.</p>
<h2 id="3-kibana-확인">3. Kibana 확인</h2>
<p><a href="http://localhost:5601/%EC%97%90">http://localhost:5601/에</a> 접속하여 Kibana 환경으로 들어가본다.
<img src="https://velog.velcdn.com/images/noh_level0/post/e8752561-43e3-4697-a096-5b57e9f4edce/image.png" width="80%" height="80%">
이러한 화면이 뜬다면 elasticsearch와 kibana가 잘 뜬 것!</p>
<h1 id="4-번외">4. 번외</h1>
<h2 id="xpacksecurityenabled은-뭘까">xpack.security.enabled은 뭘까?</h2>
<pre><code class="language-shell">- xpack.security.enabled=false</code></pre>
<p>현재 els.yml의 es에는 위와 같은 설정이 들어가 있다.
이는 Elasticsearch 보안 기능 전체를 비활성화하는 것으로, Elasticsearch의 인증, 권한, 암호화(SSL) 같은 보안 기능들을 전부 꺼버리는 설정이다.
이렇게 하게 되면 kibana에 접속할 때 로그인을 하지 않아도 되고, Docker에서 띄울 때 토큰같은 걸 안 줘도 되서 개발 환경에서는 편하지만 <strong><span style="color: red">배포 단계에서는 보안때문에 이 설정을 주어선 안 된다.</span></strong>
근데 배포도 고민을 해야 하므로... 저 설정이 없는 경우 실행을 시켜보고 싶어서 방법을 찾아보다가 아래와 같은 방법으로 해결했다.</p>
<p>먼저 els.yml은 아래와 같이 작성해야 한다.</p>
<pre><code class="language-shell">version: &#39;3.7&#39;
services:
  es:
    #image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
    build:
      context: .
      args:
        VERSION: 8.13.4
    container_name: es
    environment:
      - node.name=single-node
      - cluster.name=backtony
      - discovery.type=single-node
      #- xpack.security.enabled=false
    ports:
      - 9200:9200
      - 9300:9300
    networks:
      - es-bridge

  kibana:
    container_name: kibana
    image: docker.elastic.co/kibana/kibana:8.13.4
    environment:
      SERVER_NAME: kibana
      ELASTICSEARCH_HOSTS: https://es:9200 # 배포 시 서버 주소로 바꿔줘야 함.
      ELASTICSEARCH_SERVICEACCOUNTTOKEN: AAEAAWVsYX....
      ELASTICSEARCH_SSL_VERIFICATIONMODE: none # 배포 시 삭제해야함.
    ports:
      - 5601:5601
    # Elasticsearch Start Dependency
    depends_on:
      - es
    networks:
      - es-bridge

....(중략)

networks:
  es-bridge:
    driver: bridge</code></pre>
<hr>

<pre><code class="language-shell">docker compose -f els.yml up -d es</code></pre>
<p>일단 이 명령어로는 kibana 접속에 문제가 생기게 된다.
즉, <a href="http://localhost:5601/">http://localhost:5601/</a> 로 접속 시 “Kibana server is not ready yet.&quot; 라는 오류가 뜬다.
<img src="https://velog.velcdn.com/images/noh_level0/post/2e6af914-385a-40a7-996e-9b5fecd88ef4/image.png" width="80%" height="80%"></p>
<h2 id="해결방법">해결방법</h2>
<p>Elasticsearch는 처음 실행할 때 보안 기능이 켜져 있으면, 자동으로 <code>.security</code> 인덱스를 <strong>한 번만</strong> 생성한다. (여기에 사용자 계정, 비밀번호, 역할, 토큰 같은 보안 정보 저장)
하지만 ES가 완전히 부팅되기 전에 Kibana가 먼저 접속을 시도하면, Kibana가 &quot;보안 정보&quot;를 쓰려는 순간 .security 인덱스가 아직 없어서 예외가 발생한다.
그리고 그 순간부터</p>
<ul>
<li>인증 실패 반복</li>
<li><code>.security</code> 인덱스 생성 실패</li>
<li>토큰도 작동 안 함</li>
<li>Kibana 무한 <code>server is not ready yet</code>
  → <strong><span style="color: red">결국 다 꼬임</span></strong></li>
</ul>
<h3 id="그럼-security-인덱스는-정확히-뭐지">그럼 <code>.security</code> 인덱스는 정확히 뭐지?</h3>
<p>Elasticsearch 내부에서 <strong>사용자 인증, 권한, 보안 설정 등을 저장하는 특별한 시스템 인덱스이다.</strong></p>
<ul>
<li><code>elastic</code> 사용자 비밀번호 해시</li>
<li><code>kibana_system</code> 역할 정보</li>
<li>생성된 서비스 토큰들</li>
<li>API 키</li>
</ul>
<p>이 모든 정보가 <code>.security</code> 인덱스에 저장된다.</p>
<h3 id="depends_on--es를-해줬던거-같은데-그럼-뭐가-문제인거지">depends_on: -es를 해줬던거 같은데 그럼 뭐가 문제인거지?</h3>
<pre><code class="language-shell">depends_on:
  - es</code></pre>
<p>이건 단지 <strong>Kibana 컨테이너가 Elasticsearch 컨테이너보다 먼저 실행되지 않도록 순서를 지정</strong>해주는 역할일 뿐이다. <strong>&quot;Elasticsearch가 완전히 준비될 때까지 기다려준다&quot;는 의미는 아님!</strong></p>
<p>즉,</p>
<ul>
<li>&quot;Elasticsearch 먼저 실행해!&quot; <strong><span style="color: red">O</span></strong></li>
<li>&quot;Elasticsearch가 완전히 부팅 끝나고 사용할 수 있을 때까지 기다려!&quot; <strong><span style="color: red">X</span></strong></li>
</ul>
<h3 id="자-그럼-이제부터-비밀번호와-토큰을-만들어보자">자 그럼 이제부터 비밀번호와 토큰을 만들어보자!</h3>
<p>xpack.security.enabled=false 설정을 없애면, Elasticsearch는 보안 기능이 <strong>기본값인 true</strong>로 작동하게 되고,
그때부터는 Kibana가 Elasticsearch에 접근하려면 “토큰 또는 사용자 인증 정보”를 꼭 주어야 한다.
따라서 elastic 계정의 비밀번호와 kibana 전용 서비스 계정 토큰을 먼저 만들어 주어야 한다.
이 토큰 정보를 els.yml의 kibana 설정에 넣어주면 된다!</p>
<h4 id="1-elastic-비밀번호-설정">1. elastic 비밀번호 설정</h4>
<pre><code class="language-bash">$ docker exec -it es bash

$ bin/elasticsearch-reset-password -u elastic #elastic 계정 비밀번호 재설정</code></pre>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/bf87d7f4-f2f0-448d-9dd4-3493b8e61669/image.png" alt="">
New value: <strong><span style="color: red">Fr8uGbRxg2p=b7F3_ntL</span></strong></p>
<h4 id="2-kibana-전용-서비스-계정-토큰-만들기">2. Kibana 전용 서비스 계정 토큰 만들기</h4>
<pre><code class="language-bash">$ docker exec -it es bash # Elasticsearch 컨테이너에 들어가기

$ bin/elasticsearch-service-tokens create elastic/kibana kibana_token
  # Kibana에서 사용할 서비스 계정 토큰 생성
  # elastic/kibana : Kibana용 기본 서비스 계정 경로
  # kibana_token: 생성할 토큰의 이름 (아무거나 가능)</code></pre>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/6ff3fc7d-a049-4073-8995-70e238727c19/image.png" alt=""></p>
<h3 id="elasticsearch-실행-후-kibana를-실행하도록">elasticsearch 실행 후 kibana를 실행하도록!</h3>
<p>앞서 말했듯이 <strong>&quot;ES가 완전히 부팅되기 전에 Kibana가 먼저 접속을 시도하면, Kibana가 &quot;보안 정보&quot;를 쓰려는 순간 .security 인덱스가 아직 없어서 예외가 발생&quot;</strong>하게 된다.
따라서 elasticsearch를 먼저 docker에 띄운 후 kibana를 띄우도록 한다.</p>
<h4 id="1-elasticsearch만-실행하기">1. elasticsearch만 실행하기</h4>
<pre><code class="language-bash">docker compose -f els.yml up -d es</code></pre>
<h4 id="2-kibana만-실행하기">2. kibana만 실행하기</h4>
<pre><code class="language-bash">docker compose -f els.yml up -d kibana</code></pre>
<p>이와 같은 순서대로 실행한다면, localhost:5601 접속 시에도 문제없이 kibana에 접속할 수 있게 된다!</p>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

<blockquote>
<p><a href="https://www.flaticon.com/kr/free-icons/" title="시간표 아이콘">시간표 아이콘 제작자: Onesadya.Std - Flaticon</a></p>
</blockquote>
<p>썸네일 생성: <a href="https://velog.io/@oneook/%EC%8D%B8%EB%84%A4%EC%9D%BC-%EB%A9%94%EC%9D%B4%EC%BB%A4Thumbnail-Maker-Toy-Project">Thumbnail Maker</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Elasticsearch] Elasticsearch의 동작 방식 및 아키텍처]]></title>
            <link>https://velog.io/@noh_level0/Elasticsearch-Elasticsearch%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D-%EB%B0%8F-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</link>
            <guid>https://velog.io/@noh_level0/Elasticsearch-Elasticsearch%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D-%EB%B0%8F-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</guid>
            <pubDate>Mon, 24 Mar 2025 06:07:32 GMT</pubDate>
            <description><![CDATA[<h1 id="elastic-stack이란">Elastic Stack이란?</h1>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/eb609b88-fb69-4b9b-a8c8-8431445fb854/image.png" alt="">
데이터를 수집, 분석, 검색, 시각화할 수 있는 오픈 소스 소프트웨어 모음(오픈소스 데이터 분석 플랫폼)을 말한다.
예전에는 <strong>ELK Stack</strong>(Elasticsearch, Logstash, Kibana)이라고도 불렸는데, Beats가 추가되면서 <strong>Elastic Stack</strong>이라는 이름으로 확장되었다.</p>
<h2 id="elastic-stack-구성요소">Elastic Stack 구성요소</h2>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할 설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Beats</strong></td>
<td>경량 데이터 수집 에이전트. 서버나 클라이언트에서 직접 데이터를 수집해 Logstash나 Elasticsearch로 전달함.</td>
</tr>
<tr>
<td><strong>Logstash</strong></td>
<td>로그, 메트릭 등 다양한 데이터를 수집하고 가공한 뒤 Elasticsearch로 전달하는 데이터 파이프라인 도구.</td>
</tr>
<tr>
<td><strong>Elasticsearch</strong></td>
<td>실시간 검색과 분석을 위한 분산 검색 엔진. 모든 데이터가 이곳에 저장됨.</td>
</tr>
<tr>
<td><strong>Kibana</strong></td>
<td>Elasticsearch에 저장된 데이터를 시각화하고 대시보드로 보여주는 웹 인터페이스. 분석과 모니터링에 사용됨.</td>
</tr>
<tr>
<td># Elasticsearch 동작 방식 및 아키텍처</td>
<td></td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/e46051ce-f2f8-46ce-95f4-3cea4c960e97/image.png" alt=""></p>
<h2 id="cluster">Cluster</h2>
<p>클러스터는 하나 이상의 Elasticsearch 노드로 구성된 노드들의 집합을 의미한다.</p>
<p>안정성을 위한다면 단일 노드보다는 클러스터로 구성하는 것을 권장한다.
클러스터 내 모든 노드는 <strong>같은 클러스터 이름(</strong><span style="color: red"><code>cluster.name</code></span>)을 공유하며, 서로 데이터를 주고받고, Index와 Shard를 자동으로 분산 관리한다.-
또한 각 노드는 자신이 속한 클러스터의 데이터에만 접근하거나 데이터를 주고받을 수 있으며, 다른 클러스터의 데이터에는 접근할 수 없다.</p>
<h2 id="data-distribution-and-storage">Data Distribution and Storage</h2>
<h3 id="index">Index</h3>
<ul>
<li>DB라고 생각하면 된다. Document들이 저장되는 물리적인 공간이다.</li>
<li>즉, 이 안에는 Document라고 하는 JSON이 저장된다.</li>
<li>Document 및 Field 예시
<img src="https://velog.velcdn.com/images/noh_level0/post/4239c253-bda8-4938-958d-5ea294e8813a/image.png" alt=""></li>
</ul>
<h3 id="shard">Shard</h3>
<ul>
<li>여러 Document들이 들어있는 Index를 여러개로 나누어 주는 것.</li>
<li>Shard는 clone이 될 수도 있고, 여러개의 서버로 나누어 저장할 수도 있다.</li>
</ul>
<h3 id="replica">Replica</h3>
<ul>
<li>여러개로 나누었던 Shard를 copy하는 것.</li>
<li>즉, Primary Shard를 복제한 Shard이다.</li>
<li>원본 document가 저장되는 샤드는 <strong>primary shard,</strong> 복제본이 저장되는 샤드는 <strong>replica shard</strong> 이다.</li>
<li>replica shard는 primary shard와는 다른 노드에 저장되므로 단일 노드의 경우 replica를 사용할 수 없다.</li>
</ul>
<h3 id="요약-정리">요약 정리</h3>
<ul>
<li><strong>Elasticsearch에서는 데이터를 저장할 때 Index → Shard 구조로 나눈다.</strong></li>
<li>하나의 Index는 여러 개의 <strong>Primary Shard</strong>로 나뉘고,</li>
<li>각 <strong>Primary Shard</strong>는 <strong>자신의 복제본(Replica Shard)</strong>을 가질 수 있다.</li>
<li>이 <strong>Replica Shard는 해당 Primary Shard 전체를 복제한 것</strong>이다.</li>
<li>따라서 <strong>&quot;Replica가 Shard를 복제하는 것&quot;</strong>이다.</li>
</ul>
<table>
<thead>
<tr>
<th><strong>구성 요소</strong></th>
<th><strong>정의</strong></th>
<th><strong>관계</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Cluster</strong></td>
<td>여러 노드로 구성된 Elasticsearch 시스템 전체</td>
<td>하나 이상의 노드를 포함</td>
</tr>
<tr>
<td><strong>Node</strong></td>
<td>클러스터 내에 포함된 단일 서버(인스턴스)</td>
<td>여러 Shard를 저장함</td>
</tr>
<tr>
<td><strong>Index</strong></td>
<td>데이터를 저장하는 논리적 단위</td>
<td>여러 Shard로 구성됨</td>
</tr>
<tr>
<td><strong>Shard</strong></td>
<td>Index를 분할한 물리적 저장 단위</td>
<td>Primary / Replica로 나뉨</td>
</tr>
<tr>
<td><strong>Primary Shard</strong></td>
<td>데이터가 최초로 저장되는 샤드</td>
<td>Index 생성 시 지정한 개수만큼 생성됨</td>
</tr>
<tr>
<td><strong>Replica Shard</strong></td>
<td>Primary Shard의 복제본</td>
<td>장애 복구 및 검색 성능 향상을 위해 존재함</td>
</tr>
</tbody></table>
<br/>
<br/>
<br/>
<br/>
<br/>

<blockquote>
<p><strong>참고 사이트</strong></p>
</blockquote>
<ul>
<li><a href="https://www.youtube.com/watch?v=XZQP7fMFHMM">Elasticsearch: EP1 - Elasticsearch란 무엇인가?</a></li>
<li><a href="https://miintto.github.io/docs/es-architecture">[엘라스틱서치] Architecture</a></li>
<li><a href="https://velog.io/@koo8624/Database-Elastic-Search-2%ED%8E%B8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98Architecture">[Elasticsearch] 아키텍처(Architecture)</a><blockquote>
</blockquote>
이 페이지의 이미지 일부에는 네이버에서 제공한 나눔글꼴이 포함되어 있습니다.<blockquote>
</blockquote>
썸네일 생성: <a href="https://velog.io/@oneook/%EC%8D%B8%EB%84%A4%EC%9D%BC-%EB%A9%94%EC%9D%B4%EC%BB%A4Thumbnail-Maker-Toy-Project">Thumbnail Maker</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Elasticsearch] Elasticsearch를 사용하면 검색이 빠른 이유 - 형태소 분석과 Inverted Index]]></title>
            <link>https://velog.io/@noh_level0/Elasticsearch-%EA%B8%B0%EC%88%A0%EC%A1%B0%EC%82%AC</link>
            <guid>https://velog.io/@noh_level0/Elasticsearch-%EA%B8%B0%EC%88%A0%EC%A1%B0%EC%82%AC</guid>
            <pubDate>Mon, 24 Mar 2025 04:09:51 GMT</pubDate>
            <description><![CDATA[<h1 id="elasticsearch란">Elasticsearch란?</h1>
<p>Elasticsearch는 오픈소스 검색 및 분석 엔진이다. 대용량의 데이터를 빠르게 저장하고 검색하며 실시간으로 분석할 수 있도록 설계된 도구이다.
단어의 형태소 분석 등을 통해 기존 RDBMS에서 다루기 어려운 full text search 기능이 제공된다.</p>
<h1 id="elasticsearch가-사용되는-곳">Elasticsearch가 사용되는 곳</h1>
<p>로그 분석: 서버, 애플리케이션 로그 수집 후 검색 및 시각화</p>
<p>검색 엔진: 웹사이트나 앱 내 콘텐츠 검색 기능 구축</p>
<p>보안 분석: 이상 징후 탐지, 보안 로그 분석</p>
<p>데이터 모니터링: 실시간 시스템 상태나 지표 모니터링</p>
<h1 id="elasticsearch가-검색이-빠른-이유">Elasticsearch가 검색이 빠른 이유</h1>
<h2 id="일반적인-rdbms에서의-검색은">일반적인 RDBMS에서의 검색은?</h2>
<ul>
<li>텍스트를 검색하기 위해서 <strong><span style="color: red"><code>LIKE</code></span></strong> 연산을 사용한다.</li>
<li>즉, 패턴 매칭으로 탐색을 하기 때문에 테이블에 저장된 <strong><span style="color: red"><code>모든 데이터를 탐색하면서</code></span></strong> 주어진 패턴과 일치하는지 따지게 된다.</li>
</ul>
<h2 id="역-인덱스-inverted-index"><strong>역 인덱스 (Inverted Index)</strong></h2>
<ul>
<li>Index<ul>
<li>흔히 책 앞 부분의 목차로 비유한다.</li>
<li>DB의 PK처럼 각 챕터의 제목을 나타낸다.</li>
</ul>
</li>
<li>Inverted Index<ul>
<li>책 뒷부분의 찾아보기에 비유할 수 있다.</li>
<li>특정 키워드가 몇 페이지에 등장했는지 나타낸다.</li>
<li>Inverted Index는 특정 키워드로 이를 포함하고 있는 문서들에 대한 PK를 맵핑하는 테이블을 사용하며, 이를 활용해 빠른 검색이 가능하도록 해준다.</li>
<li>검색엔진에서 Inverted Index 테이블은 주로 <strong><code>BTree</code>, <code>Trie</code>, <code>Hash Table</code></strong> 등의 자료구조를 활용하여 구현된다.</li>
</ul>
</li>
</ul>
<h2 id="elasticsearch에서의-inverted-index-방식"><strong>Elasticsearch에서의 Inverted Index 방식</strong></h2>
<ul>
<li>Elasticsearch를 포함한 대부분의 검색엔진에서는 <strong>형태소 분석</strong>을 통해 문서 내에서 핵심 키워드(<code>Term</code>)을 추출 → <strong>Inverted Index 테이블을 업데이트</strong>한다. 
형태소 분석은 언어에 대한 깊은 이해와 많은 연산을 필요로 한다. 따라서 이 처리 과정의 효율성과 정확성은 검색 엔진의 성능과도 직결된다.</li>
<li>“apple”이라는 단어를 검색할 때, 일반적인 RDBMS에서는 테이블의 전체 행을 탐색한다. 하지만 Inverted Index의 구조에서는 “apple”이라는 단어가 가리키는 도큐먼트가 무엇인지 확인을 하면 된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/00897595-14bc-4041-8c9f-bfa9a63d3763/image.png" alt=""></p>
<ul>
<li>이러한 Inverted Index을 활용하면 시간 복잡도가 $O(1)$에 수렴하게 된다.</li>
<li>Term을 생성해 Inverted Index 작업을 처리하는 데 꽤 오랜 시간이 걸리게 되며, 삽입된 <strong>도큐먼트가 검색 가능한 상태가 될 때 까지 약간의 대기 시간</strong>이 존재하게 된다. 이를 <strong>NRT(near real-time)</strong> 이라고 한다.</li>
</ul>
<h1 id="mysql-full-text와-elasticsearch의-차이점">MySQL Full-Text와 Elasticsearch의 차이점</h1>
<table>
<thead>
<tr>
<th>구분</th>
<th>MySQL Full-Text</th>
<th>Elasticsearch</th>
</tr>
</thead>
<tbody><tr>
<td>역할</td>
<td>기본 검색 기능</td>
<td>전문 검색 시스템</td>
</tr>
<tr>
<td>정확도</td>
<td>낮음 (단어 일치 위주)</td>
<td>높음 (의미 분석, 유사도까지)</td>
</tr>
<tr>
<td>기능</td>
<td>단어 포함 여부만 확인</td>
<td>형태소 분석, 유사도 계산, 오타 교정 등</td>
</tr>
<tr>
<td>사용처</td>
<td>작은 웹사이트, 단순 검색</td>
<td>포털, 쇼핑몰, 대용량 검색 서비스</td>
</tr>
<tr>
<td>속도</td>
<td>느려질 수 있음</td>
<td>빠르고 확장성 있음</td>
</tr>
</tbody></table>
<br/>
<br/>
<br/>
<br/>
<br/>

<blockquote>
<p><strong>참고 사이트</strong></p>
</blockquote>
<ul>
<li><a href="https://hudi.blog/elasticsearch-inverted-index/">Elasticsearch는 왜 검색속도가 빠를까? - Inverted Index</a></li>
<li><a href="https://velog.io/@koo8624/Database-Elastic-Search-1%ED%8E%B8-%EC%97%AD%EC%83%89%EC%9D%B8Inverted-Index%EA%B3%BC-%ED%98%95%ED%83%9C%EC%86%8C-%EB%B6%84%EC%84%9D">[Elasticsearch] 역색인(Inverted Index)과 형태소 분석</a><blockquote>
</blockquote>
썸네일 생성: <a href="https://velog.io/@oneook/%EC%8D%B8%EB%84%A4%EC%9D%BC-%EB%A9%94%EC%9D%B4%EC%BB%A4Thumbnail-Maker-Toy-Project">Thumbnail Maker</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SSAFY 공통] OpenVidu 3을 ec2 서버에 올려보자!]]></title>
            <link>https://velog.io/@noh_level0/SSAFY-%EA%B3%B5%ED%86%B5-OpenVidu-3%EC%9D%84-ec2-%EC%84%9C%EB%B2%84%EC%97%90-%EC%98%AC%EB%A0%A4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@noh_level0/SSAFY-%EA%B3%B5%ED%86%B5-OpenVidu-3%EC%9D%84-ec2-%EC%84%9C%EB%B2%84%EC%97%90-%EC%98%AC%EB%A0%A4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 27 Jan 2025 18:17:53 GMT</pubDate>
            <description><![CDATA[<p>OpenVidu... 튜토리얼 코드도 분석하기 쉽지 않았는데, ec2에 올리는게 더 쉽지 않았다ㅜㅜㅜ
심지어 OpenVidu v3가 <strong><span style="color: red">2024년 6월</span></strong>에 출시된거라 참고 자료 찾기도 어렵더라ㅠㅠ.. 심지어 이미 v3를 기반으로 어느정도 코드는 짜 놓은 상태라 더더욱 포기하기 싫었다..!!
내가 봤던 ec2 서버에 올리는 자료들은 전부 다 OpenVidu v2 관련한 것이여서 v3랑은 달라진 점이 많아 많이 애를 먹었다. 그래서 v3를 ec2에 어떻게 올릴 수 있었는지 3일간의 과정을 공유해보고자 한다..</p>
<h1 id="공식-문서">공식 문서</h1>
<p><a href="https://openvidu.io/latest/docs/self-hosting/single-node/on-premises/install/#port-rules">OpenVidu Single Node Installation: On-premises</a></p>
<h1 id="포트를-열자">포트를 열자!</h1>
<p>일단 OpenVidu를 사용하기 위해서는 지켜야 할 포트 규칙들이 많다. 이 설정을 통해 지정된 포트를 통해 들어오는 요청을 허용할 수 있도록 만들어 주어야 한다.
아래는 포트를 설정할 수 있는 명령어다.</p>
<pre><code class="language-shell">$ sudo ufw allow 80/tcp
$ sudo ufw allow 50000:60000/udp</code></pre>
<p>443, 1935, 7881, 7885, 9000 등의 포트들도 공식문서에서 요구하는 대로 위 명령어를 활용해 설정해주자!</p>
<h1 id="openvidu-설치">OpenVidu 설치!</h1>
<pre><code class="language-shell">$ sh &lt;(curl -fsSL http://get.openvidu.io/community/singlenode/latest/install.sh)</code></pre>
<p>공식문서에서 알려주는 설치 명령어다!
여러 자료들을 찾아보니 보통 opt 라는 경로에 설치해주는 것이 일반적이라고 하길래 opt 경로에서 위의 명령어를 실행해주었다.
<img src="https://velog.velcdn.com/images/noh_level0/post/01dd60d1-6575-4999-a724-1cb3f685bcbc/image.png" alt=""></p>
<p>설치가 완료되면 이런 화면을 볼 수 있다!</p>
<h1 id="설치-다-했는데-왜-openvidu가-자꾸-docker에서-실행이-안-되는건가요">설치 다 했는데 왜 OpenVidu가 자꾸 Docker에서 실행이 안 되는건가요..?</h1>
<p>설치를 다 했으면 아래의 명령어를 실행하라고 해서 했는데...</p>
<pre><code class="language-shell">$ systemctl start openvidu</code></pre>
<p>이걸 실행해도 dashboard 같은 기본 화면조차 안 들어가졌다ㅜㅜ
확인해보니 아예 docker 실행에서부터 오류가 나서 계속 안되는거였더라..
뭘 찾아봐야할지도 모르겠고 그냥 <span style='background-color: yellow'>GPT</span>랑 씨름함..ㅎ</p>
<h2 id="1-openviduservice-내용-수정">1. openvidu.service 내용 수정</h2>
<pre><code class="language-shell">$ nano /etc/systemd/system/openvidu.service</code></pre>
<p>ExecStartPre같은 항목에서 docker-compose를 실행하는 경로가 올바르지 않았다.
따라서 해당 내용을 수정!
<img src="https://velog.velcdn.com/images/noh_level0/post/a9c720c2-32f3-4f4d-a870-6fb78ccc58e8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/a9ba42e1-68d1-4454-93bc-db14010e6ebf/image.png" alt=""></p>
<pre><code class="language-shell">$ systemctl start openvidu</code></pre>
<p>이 파일을 수정한 후에 바로 위의 명령어를 수행하면 오류가 난다.</p>
<pre><code class="language-shell">$ systemctl daemon-reload</code></pre>
<p>이 명령어를 먼저 수행해주자!</p>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/fe857155-0690-49dd-ab5f-e59e8542dea5/image.png" alt=""></p>
<h2 id="2-계속해서-mongo-관련-환경변수들을-못-가져오는데">2. 계속해서 MONGO 관련 환경변수들을 못 가져오는데..?</h2>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/86cb6ba0-477a-4a0a-9c5f-e8243fa9c7b2/image.png" alt=""></p>
<p>1번까지 했는데도 자꾸 Docker Compose에서 docker-compose.yaml에서 작성된 MONGO 관련 환경변수들을 못 가져오는 오류들이 발생했다.
아무리 오류를 살펴보고 관련 파일 내용들을 읽어봐도 도대체 왜 MONGO에서만 그런 문제가 발생하는건지 찾을 수가 없더라..
docker-compose.yaml에 그냥 MONGO_ADMIN_USERNAME, MONGO_ADMIN_PASSWORD 등 문제가 발생한 곳에 바로 값을 넣어줄까 하다가 <del>그건 좀..;;</del> 싶어서 <strong>다른 방법을 찾.았.다!</strong>
<strong><span style="color:red">이때부터 GPT랑 씨름 시작</span></strong></p>
<h2 id="3-docker-compose-버전-문제">3. Docker Compose 버전 문제</h2>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/7c1ae0cc-ba24-4a93-b63c-f5e149910b2e/image.png" alt=""></p>
<p>Docker의 버전 문제라고는 생각도 못하고 OpenVidu 관련 설정 파일들만 계속 수정을 하면서 3일 동안 테스트를 했는데, 계속해서 해결이 안 되었다. 그러던 중 확인해보게 된 Docker 버전.</p>
<p>Docker 관련 버전들을 확인하니 docker --version은 비교적 최신이라 괜찮은데, docker-compose의 버전이 너무 낮았다.
OpenVidu의 docker-compose.yaml은 v2.x 이상에서 사용하는 기능들을 포함하고 있는데, v1.x에서는 이를 해석하지 못해서 그렇다고 한다.(<del><span style="color:pink">GPT 왈</span></del>)
그래서 docker-compose의 버전을 한번 업그레이드 해보았다.</p>
<h2 id="4-docker-compose-버전-업그레이드">4. Docker Compose 버전 업그레이드</h2>
<h3 id="4-1-기존의-docker-compose-삭제">4-1. 기존의 docker-compose 삭제</h3>
<pre><code class="language-shell">$ sudo apt-get remove docker-compose -y</code></pre>
<h3 id="4-2-jq-설치">4-2. jq 설치</h3>
<pre><code class="language-shell">$ sudo apt install jq</code></pre>
<p>Docker Compose의 최신 버전을 가져오기 위해 설치한다.
이를 활용해 GitHub API로부터 JSON 데이터를 처리할 때 사용한다.</p>
<h3 id="4-3-최신-docker-compose-설치">4-3. 최신 Docker Compose 설치</h3>
<pre><code class="language-shell">$ VERSION=$(curl --silent https://api.github.com/repos/docker/compose/releases/latest | jq .name -r)
// curl 명령어로 Docker Compose의 GitHub 릴리스 API에서 최신 릴리스 정보를 가져옴.

$ DESTINATION=/usr/bin/docker-compose
//Docker Compose를 설치할 경로를 지정

$ sudo curl -L https://github.com/docker/compose/releases/download/${VERSION}/docker-compose-$(uname -s)-$(uname -m) -o $DESTINATION
//curl 명령으로 Docker Compose의 최신 버전을 다운로드

$ sudo chmod 755 $DESTINATION
//다운로드한 Docker Compose 바이너리에 실행 권한을 부여</code></pre>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/cdf7a538-b0d9-48de-911f-cb3a1691294e/image.png" alt=""></p>
<p>설치를 하다보면 중간에 이런 화면이 뜰 수도 있는데, 난 그냥 기본 설정을 따라서 &lt;Ok&gt;만 선택해주었다.</p>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/50d08b4f-287c-4903-81fe-0a861ff2f7d2/image.png" alt=""></p>
<p>최종적으로 이렇게 버전을 업그레이드 해주었다!</p>
<h1 id="드디어-성공">드디어 성공!</h1>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/9061bd94-60a9-4652-ad8d-84aa16e8a428/image.png" alt=""></p>
<p>성공적으로 OpenVidu가 ec2에 올라갔다면, https://<DOMAIN_NAME>/ 으로 접속했을 때 이 같은 화면이 나온다!</p>
<h1 id="마지막으로-openvidu관련-컨테이너들을-중지시켜보자">마지막으로 OpenVidu관련 컨테이너들을 중지시켜보자!</h1>
<p>혹시 몰라서 ec2 서버의 과금이 걱정되어 OpenVidu 컨테이너들을 중지시키려고 했었다.
그런데 단순히 docker-compose down으로는 중지가 되지 않네..?</p>
<p>docker-compose.yaml 파일을 뒤적거려보니 <span style="color:red"><strong>도커에서 실행이 중지가 되면 자동으로 재시작이 되도록 작성이 되어있는 것을 발견</strong></span>했다.</p>
<p>따라서 어떻게 중지시켜야 할까.. 고민하다가 공식문서에서 OpenVidu를 실행할 때 사용한다고 알려준 명령어를 반대로 작성하면 되는거 아닐까? 하다가 방법을 찾았다!</p>
<p>결론적으로</p>
<pre><code class="language-shell">$ systemctl stop openvidu</code></pre>
<p>이 명령어로 중지시킬 수 있다! </p>
<p>다시 실행하고 싶다면 아래와 같은 명령어로 실행하면 된다!</p>
<pre><code class="language-shell">$ systemctl start openvidu</code></pre>
<h1 id="ec2에-openvidu를-배포해보며">ec2에 OpenVidu를 배포해보며...</h1>
<p>정말 OpenVidu 3버전 관련해서 배포 관련 문서를 찾아보기가 어려웠다.
그러다가 어쩔 수 없이 GPT에도 의존해보기도 하고 나도 OpenVidu 관련 문서나 설정 파일들을 여러번 읽어보게 되었다.</p>
<p>하지만 GPT 조차도 관련 문서들을 찾아보기 어렵다보니 해결책을 준다고 주는 게 다 v2 관련한거더라😂
그래서 계속해서 내 상황을 다시 설명하고 문제점을 찾아보고... 나도 OpenVidu 관련한 설정파일들을 꼼꼼히 살펴보고.. 이런 상황의 연속이였는데 결국 해결할 수 있게 되서 너무나 뿌듯하다!!</p>
<p>계속해서 반복적인 대화를 진행하면서 GPT의 성능을 의심하기도 하였지만..ㅋㅋㅋㅋ
결정적으로 docker compose의 버전 문제를 찾아준 게 GPT였다.
정<del>\</del> 문서를 찾아보기 힘들다면 GPT를 한번 끝까지 쪼아보자!! 돌고돌아가는 과정인 것 같기는 하지만.. GPT가 주는 여러 방안들 중에서 하나를 건질 수 있다면 좋은 게 아닐까..?(<strong><span style="color:red"><del><em>해답 찾기까지 너무 오래 걸리긴 함;;</em></del></span></strong>)</p>
<p><br/><br/><br/><br/></p>
<blockquote>
<p>이 페이지의 이미지 일부에는 네이버에서 제공한 나눔글꼴이 포함되어 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2024년 상반기 회고록] 코테탈, 면탈에 이은 SSAFY 입과(면스X)...]]></title>
            <link>https://velog.io/@noh_level0/2024%EB%85%84-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EC%BD%94%ED%85%8C%ED%83%88-%EB%A9%B4%ED%83%88%EC%97%90-%EC%9D%B4%EC%9D%80-SSAFY-%EC%9E%85%EA%B3%BC%EB%A9%B4%EC%8A%A4X</link>
            <guid>https://velog.io/@noh_level0/2024%EB%85%84-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EC%BD%94%ED%85%8C%ED%83%88-%EB%A9%B4%ED%83%88%EC%97%90-%EC%9D%B4%EC%9D%80-SSAFY-%EC%9E%85%EA%B3%BC%EB%A9%B4%EC%8A%A4X</guid>
            <pubDate>Sun, 07 Jul 2024 13:55:09 GMT</pubDate>
            <description><![CDATA[<p>우선 이번 상반기에 지원한 기업은 총 14군데..
현실을 잘 모르고 눈만 높아서 고르고 골랐다..ㅋㅋㅋㅋ</p>
<p>서탈, 코테탈이 반복되다가 이대로 정체되면 안 될 것 같다는 생각과 SSAFY라는게 수료하면 서류에서 우대해주는 기업들이 많다더라는 얘기에 무작정 지원하게 되었다. 지원을 했음에도 불구하고 내가 원하는건 취업인데 이게 맞나.. 망설이면서 면접까지 봤던거 같다. 결국엔 입과까지 가버리게 되었지만..ㅎ</p>
<h2 id="1-서합-후-코테-본-기업">1. 서합 후 코테 본 기업</h2>
<p><del><em><strong>아니 근데 중소, 중견 라인은 내 서류를 확인도 안 하는거 같던데..? 사람인에서 보니까 이력서 확인조차 안 한 걸로 뜸..</strong></em></del></p>
<ul>
<li>네이버 - <del>_<span style="color: red">네이버는 서류+코테가 첫 전형임</span>_</del></li>
<li>CJ올리브네트웍스</li>
<li>KB증권 - 1차 면접에 합격하면 인턴 기회가 주어졌었다..!  <br/>
  * 네이버
  내 인생 첫 코테는 네이버 코테였다. 사실 코테라는 것에 감을 잡지 못했을 때라 그냥 무작정 테스트케이스 결과만 잘 나오면 되겠지 하고 봤었다..ㅋㅋ
  **3문제 중에 2문제를 풀고** 테스트 케이스는 다 맞췄는데 아마도 채점에서 테스트 케이스가 추가되면서 **시간초과**가 나지 않았을까 싶다ㅠㅠ 결과는 탈락!
  <br/>
  * CJ올리브네트웍스
  사실상 첫 대기업 서류 통과다ㅠㅠ 감사할따름.. 그래서 그런지 더 긴장하고 보게 됐는데 웬걸.. 네이버보다 못 봤다.. 문제 자체는 더 쉬웠는데.. **~~_시험 끝나고 나니까 풀이가 생각남.._~~** 결론은 탈락!
  <br/>
  * KB증권
  처음으로 코테에 **합격**한 기업! 그래서 1차 면접까지 갔다~~
  문제는 알고리즘+SQL 이었다. 합격시켜 주셔서 감사..🥹
 <br/>
## 2. 1차 면접</li>
<li>KB증권(<strong>인터뷰 + PT + 토의</strong>)
코테에 합격해서 처음으로 면접을 가봤다..! 너무 값진 경험이였지만 결국엔 탈락해서 아쉬운.. 분명 면접 당시엔 분위기가 좋았다고 생각했는데 내 기분탓이었나 보다.. 역시 면까몰..?
PT랑 토의는 내가 예상했던게 나와서 쉽게 했다고 생각했는데 면접관 분들이 봤을 땐 아니었나 보다ㅠㅠ 그래도 다양한 면접 경험을 해볼 수 있어서 좋았다..!</li>
</ul>
<h2 id="3-ssafy-입과">3. SSAFY 입과</h2>
<p>CJ올리브네트웍스 코테를 치루고 자괴감에 빠져 선택했다..ㅎ
그래도 SSAFY가면 코테능력은 오를 수 있다기에.. 그 말에 현혹된 것도 조금 있었다.
그치만 내 본래 목표는 취업인걸.. 또 다시 어디를 들어가서 교육을 듣는다는게 조금 납득하기 어려워서 남들 다 하는 SSAFY 면접 스터디도 안 했다. 면접도 그냥 경험 쌓는다는 느낌으로 보러가자 싶어서 보러갔었다. 그런데 합격을 했길래 운명인갑다.. 하는 중
<strong>SSAFY를 준비하는 사람들이 있다면 굳이 꼭 스터디를 안 해도 합격할 수 있다고 말씀드리고 싶다..!</strong></p>
<p><img src = "https://velog.velcdn.com/images/noh_level0/post/7c0a534e-fb02-4f23-8e86-79eff03f5b77/image.png"/></p>
그래도 소속감도 생기고 달마다 고정 수입이 생겼다는 것에 만족하고 있다.. 사람들두 다 좋으시구🥹

<h2 id="4-앞으로의-목표">4. 앞으로의 목표</h2>
<p>일단 SSAFY를 다니면서도 꾸준히 멈추지 않고 지원서는 계속 넣을 예정이다.
SSAFY 입과 후에도 이미 2개나 지원서 넣었구..!
SSAFY 다니면서 알고리즘 고수도 될 작정이다~!
<span style="background-color:#C0FFFF"><strong>삼성 SW 역량 테스트 B형도 따보자!!!</strong></span></p>
<p><span style="background-color:#fff5b1"><strong>취준하시는 분들 다같이 화이팅해요~~!!</strong></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BAEKJOON] 10799 쇠막대 - 파이썬]]></title>
            <link>https://velog.io/@noh_level0/BAEKJOON-10799-%EC%87%A0%EB%A7%89%EB%8C%80-%ED%8C%8C%EC%9D%B4%EC%8D%AC</link>
            <guid>https://velog.io/@noh_level0/BAEKJOON-10799-%EC%87%A0%EB%A7%89%EB%8C%80-%ED%8C%8C%EC%9D%B4%EC%8D%AC</guid>
            <pubDate>Tue, 16 Apr 2024 02:51:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="백준-10799-문제-링크"><a href="https://www.acmicpc.net/problem/10799">백준 10799 문제 링크</a></h4>
</blockquote>
<h1 id="💡-1-문제-정의">💡 1. 문제 정의</h1>
<p>이 문제는 여는 괄호( &#39;<strong>(</strong>&#39; )와 닫는 괄호( &#39;<strong>)</strong>&#39; )로 구성된 입력을 보고 레이저 또는 막대기로 판단해 얻을 수 있는 총 막대기의 개수를 구하는 문제이다.
<br/></p>
<h1 id="🤔-2-해결-방법">🤔 2. 해결 방법</h1>
<p><a href="https://velog.io/@noh_level0/%EA%B7%B8%EB%9E%98%ED%94%84-%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98BFSDFS">스택이란?</a>
스택(stack)을 활용하여 현재까지 잘린(or 안 잘린) 막대기의 개수를 판단하는 것에 활용하도록 한다.
<br/></p>
<h1 id="📑-3-문제-풀이">📑 3. 문제 풀이</h1>
<ol>
<li>초기값으로 스택에 &#39;(&#39;를 하나 넣는다.</li>
<li>입력 문자열의 <strong>2번째부터</strong> 하나씩 살펴본다.</li>
<li>현재 문자가 &#39;(&#39;라면 스택에 append 한다.</li>
<li>현재 문자가 &#39;)&#39;이고, 바로 이전 문자가 &#39;(&#39;였다면 이것은 <strong>레이저</strong>이다. 따라서 이전 단계에서 &#39;(&#39;로 인해 스택에 넣어주었던 값을 하나 pop 해준다. 또한 <strong>레이저</strong>가 지나가므로 <strong>현재까지 파악된 막대기의 앞부분은 잘릴 것이다</strong>. 따라서 스택의 길이만큼의 막대기가 생긴다.</li>
<li>현재 문자가 &#39;)&#39;이고, 바로 이전 문자도 &#39;)&#39;였다면 이것은 <strong>막대기의 끝이다</strong>. 따라서 스택에 넣어주었던 값을 하나 pop 해준다. 또한 레이저가 지나갔든 안 지나갔든 잘린(or 안 잘린) <strong>막대기는 하나 더 생겼다고 볼 수 있다</strong>.</li>
</ol>
<pre><code class="language-python">bar = input()

stack = [&#39;(&#39;]
cnt = 0
for b in range(1, len(bar)):
    if bar[b] == &#39;(&#39;:
        # 현재 괄호가 &#39;(&#39;라면 쇠막대의 시작이거나 레이저의 시작이다.
        stack.append(&#39;(&#39;)
    else:
        if bar[b - 1] == &#39;(&#39;:
            # 현재 괄호가 &#39;)&#39;이고 이전 괄호가 &#39;(&#39;이라면 레이저이다.
            stack.pop()
            cnt += len(stack)
        else:
            # 현재 괄호가 &#39;)&#39;이고 이전 괄호가 &#39;)&#39;이라면 쇠막대의 끝이다.
            stack.pop()
            cnt += 1

print(cnt)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BAEKJOON] 2805 나무 자르기 - 파이썬]]></title>
            <link>https://velog.io/@noh_level0/BAEKJOON-2805-%EB%82%98%EB%AC%B4-%EC%9E%90%EB%A5%B4%EA%B8%B0-%ED%8C%8C%EC%9D%B4%EC%8D%AC</link>
            <guid>https://velog.io/@noh_level0/BAEKJOON-2805-%EB%82%98%EB%AC%B4-%EC%9E%90%EB%A5%B4%EA%B8%B0-%ED%8C%8C%EC%9D%B4%EC%8D%AC</guid>
            <pubDate>Mon, 15 Apr 2024 13:47:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="백준-2805-문제-링크"><a href="https://www.acmicpc.net/problem/2805">백준 2805 문제 링크</a></h4>
</blockquote>
<h1 id="💡-1-문제-정의">💡 1. 문제 정의</h1>
<p>이 문제는 이진 탐색 기법을 활용하여 절단기에 설정할 수 있는 높이의 최댓값을 구하는 문제이다.
시간초과를 잘 고려해서 풀어주어야 해결이 되는 문제였다.
<br/></p>
<h1 id="🤔-2-해결-방법">🤔 2. 해결 방법</h1>
<p><a href="https://velog.io/@noh_level0/%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98">이진 탐색</a>을 활용하여 절단기의 높이를 시작점, 중간점, 끝점을 이용해 가장 최적의 절단기의 높이를 구해준다. 
<br/></p>
<h1 id="📑-3-문제-풀이">📑 3. 문제 풀이</h1>
<ul>
<li>계속해서 실패했던 이유가 바로 시간초과였다.</li>
<li>실패했던 요인은 바로 <span style="color: red">2가지</span>였다.<ol>
<li>입력을 input() 함수로 받았다.</li>
<li>while문 안의 for문에서 조건문을 &quot;if (t - mid) &gt;= 0&quot;로 작성하여 연산과정을 추가하였다.</li>
</ol>
</li>
<li>따라서 이 <span style="color: red">2가지 요인을 다음과 같이 변경</span>하였다.<ol>
<li>sys 라이브러리를 활용하여 입력받는다. sys.stdin.readline()은 input() 함수보다 빠르게 동작한다.(input()은 개행 문자 제거 및 문자열 변환 과정이 포함되기 때문에 상대적으로 느리다.)</li>
<li>조건문을 &quot;if t &gt; mid&quot;로 변경하여 작성한다.</li>
</ol>
</li>
</ul>
<p><span style="color: red"><strong>문제 풀 때 되도록이면 sys 라이브러리를 사용하자!!</strong></span></p>
<pre><code class="language-python">import sys

input = sys.stdin.readline

n, m = map(int, input().split()) # n: 나무의 수, m: 집으로 가져가려고 하는 나무의 길이

tree = list(map(int, input().split())) # 나무들의 높이

start = 0
end = max(tree)

result = 0

while start &lt;= end:
    mid = (start + end) // 2 # 절단기의 높이 설정

    carry = 0
    for t in tree:
        if t &gt; mid:
            carry += (t - mid) # 절단기로 자른 후 가져갈 수 있는 나무의 길이를 더한다.

    if carry &gt;= m:
        result = mid
        start = mid + 1
    else:
        end = mid - 1

print(result)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[이진 탐색 알고리즘]]></title>
            <link>https://velog.io/@noh_level0/%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@noh_level0/%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Mon, 15 Apr 2024 10:35:52 GMT</pubDate>
            <description><![CDATA[<h1 id="1-이진-탐색이란">1. 이진 탐색이란</h1>
<ul>
<li>순차 탐색: 리스트 안의 특정 데이터를 찾기 위하여 데이터를 하나씩 확인한다.</li>
<li>이진 탐색: <strong>정렬된 리스트</strong> 내에서 <span style="color: red">탐색의 범위를 절반씩 줄여가며</span> 원하는 데이터를 찾는 방법이다.<ul>
<li>시작점, 중간점, 끝점을 이용하여 탐색의 범위를 설정한다.</li>
</ul>
</li>
<li>시간 복잡도: 단계마다 탐색의 범위를 2로 나누어 탐색하는 것과 동일하므로 $$log_{2}N$$에 비례한다. 따라서 시간 복잡도는 $$O(logN)$$을 보장한다.</li>
</ul>
<h1 id="2-이진-탐색의-동작법">2. 이진 탐색의 동작법</h1>
<p><img src = "https://velog.velcdn.com/images/noh_level0/post/a946beb0-9a3b-43f3-8814-444053981ff1/image.png"/></p>

<h1 id="3-이진-탐색-구현with-python">3. 이진 탐색 구현(with Python)</h1>
<h3 id="31-재귀-함수로-구현한-이진-탐색">3.1 재귀 함수로 구현한 이진 탐색</h3>
<pre><code class="language-python">def binary_search(array, target, start, end):
    if start &gt; end:
        return None
    mid = (start + end) // 2

    if array[mid] == target:
        return mid
    elif array[mid] &gt; target:
        return binary_search(array, target, start, mid-1)
    else:
        return binary_search(array, target, mid+1, end)

n, target = list(map(int, input().split()))

array = list(map(int, input().split()))

result = binary_search(array, target, 0, n-1)
if result == None:
    print(&quot;원소가 존재하지 않습니다.&quot;)
else:
    print(result+1,&quot;번째에 값이 있습니다.&quot;)</code></pre>
<h3 id="32-반복문으로-구현한-이진-탐색">3.2 반복문으로 구현한 이진 탐색</h3>
<pre><code class="language-python">def binary_search(array, target, start, end):
    while start &lt;= end:
        mid = (start+end) // 2
        if array[mid] == target:
            return mid
        elif array[mid] &gt; target:
            end = mid - 1
        else:
            start = mid + 1
    return None

n, target = list(map(int, input().split()))

array = list(map(int, input().split()))

result = binary_search(array, target, 0, n-1)
if result == None:
    print(&quot;원소가 존재하지 않습니다.&quot;)
else:
    print(result+1,&quot;번째에 값이 있습니다.&quot;)</code></pre>
<p><br/><br/><br/></p>
<blockquote>
<p>그림 폰트 출처: 제주특별자치도, 제주서체, <a href="https://www.jeju.go.kr/jeju/symbol/font/infor.htm">https://www.jeju.go.kr/jeju/symbol/font/infor.htm</a>
참고 서적: 이것이 취업을 위한 코딩 테스트다 with 파이썬, 나동빈, 한빛미디어
참고 강의: <a href="https://www.youtube.com/watch?v=7C9RgOcvkvo&amp;list=PLRx0vPvlEmdAghTr5mXQxGpHjWqSz0dgC&amp;index=3">나동빈님 유튜브 강의</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BAEKJOON] 3190 뱀 - 파이썬]]></title>
            <link>https://velog.io/@noh_level0/BAEKJOON-3190-%EB%B1%80-%ED%8C%8C%EC%9D%B4%EC%8D%AC</link>
            <guid>https://velog.io/@noh_level0/BAEKJOON-3190-%EB%B1%80-%ED%8C%8C%EC%9D%B4%EC%8D%AC</guid>
            <pubDate>Mon, 15 Apr 2024 02:56:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="백준-3190-문제-링크"><a href="https://www.acmicpc.net/problem/3190">백준 3190 문제 링크</a></h4>
</blockquote>
<h1 id="💡-1-문제-정의">💡 1. 문제 정의</h1>
<p>이 문제는 주어진 보드 내에서 적절하게 뱀의 위치를 변경하고 길이를 늘려주는 문제이다.
BFS 알고리즘으로 큐를 활용하여 해결해도 되고, 입력된 보드 자체를 활용하여 구현할 수도 있다.
<br/></p>
<h1 id="🤔-2-해결-방법">🤔 2. 해결 방법</h1>
<p>입력된 보드 자체를 활용하여 해결$$^1$$, BFS 알고리즘으로 해결$$^2$$ 이 2가지 방법으로 문제를 해결해본다.
<br/></p>
<h1 id="📑-3-문제-풀이">📑 3. 문제 풀이</h1>
<h2 id="31-입력된-보드-자체를-활용하여-해결해보기">3.1 입력된 보드 자체를 활용하여 해결해보기</h2>
<p>자료구조 알고리즘에 익숙하지 않아서 처음에는 쌩으로 그냥 구현했다...
이렇게 하다보니 변수들이 너무 많아지고 머리속에서 너무 복잡해지더라ㅜㅜ</p>
<pre><code class="language-python">n = int(input())
k = int(input())

board = [[0]*n for _ in range(n)] #사과의 위치를 담는다.
board[0][0] = 1 #처음 위치는 무조건 방문처리

for _ in range(k):
    a, b = map(int, input().split())
    board[a-1][b-1] = -1 #사과의 위치에 -1이라는 값을 넣는다.

l = int(input()) #뱀의 방향 변환 횟수
X = []
move = []
for _ in range(l):
    a, b = input().split()
    X.append(int(a)) #x초 후 방향전환
    move.append(b) #&#39;L&#39;은 왼쪽으로 90도 방향전환
                   #&#39;D&#39;는 오른쪽으로 90도 방향전환

dx = [0, -1, 0, 1]
dy = [1, 0, -1, 0]
# 순서대로 오, 위, 왼, 아
direc = 0 #처음 방향은 오른쪽
x, y = 0, 0 #뱀의 머리 좌표
tail = [0, 0] #뱀의 꼬리 위치
time = 1 #게임 시간
flag = False #이중 반복문을 종료할지 판단한다.
m_tail = 1
snake = 1
for n1 in range(n):
    for n2 in range(n):
        if (time-1) in X:
            if move[X.index(time-1)] == &#39;L&#39;:
                # 왼쪽으로 회전
                direc += 1
                if direc &gt; 3:
                    direc = 0
            elif move[X.index(time-1)] == &#39;D&#39;:
                # 오른쪽으로 회전
                direc -= 1
                if direc &lt; 0:
                    direc = 3
        x = x + dx[direc]
        y = y + dy[direc]
        if x &gt;= 0 and x &lt; n and y &gt;= 0 and y &lt; n:
            if board[x][y] != -1 and board[x][y] &lt;= 0:
                # 자기자신의 몸과 부딪히지 않았다면 방문처리한다.
                time += 1

                board[x][y] = snake
                board[tail[0]][tail[1]] = 0
                snake += 1

                for b in range(n):
                    if m_tail in board[b]:
                        t_x = b
                        t_y = board[b].index(m_tail)
                tail = [t_x, t_y]
                m_tail += 1

            elif board[x][y] == -1 and board[x][y] &lt;= 0:
                # 사과가 있고 벽이나 자기자신의 몸과 부딪히지 않았다면 방문처리한다.
                time += 1

                board[x][y] = snake
                snake += 1
            else:
                # 자기자신의 몸과 부딪혔다면 게임을 끝낸다.
                flag = True
                break
        else:
            # 벽에 부딪혔다면 게임을 끝낸다.
            flag = True
            break
    if flag:
        break
print(time)</code></pre>
<p>사과가 있는 위치에는 -1을 넣어주고 뱀의 몸통이 없는 곳에는 전부 0으로 처리해주었다.
뱀이 지나가는 곳이라면 &#39;뱀의 꼬리 -&gt; 뱀의 머리&#39;까지 1씩 숫자를 증가시키며 해당 보드 위치에 넣어주었다.
따라서 뱀이 사과를 먹지 못해 길이가 길어질 수 없는 상황이라면 뱀의 꼬리부분 위치에 있던 숫자를 0으로 바꿔주었다.
뱀의 꼬리부분 숫자는 0과 -1을 제외한 가장 작은 값으로 m_tail 변수를 사용해 보드 내 m_tail이 존재하는 위치를 찾아 꼬리 위치를 찾아냈다.</p>
<br/>

<h2 id="32-bfs-알고리즘으로-해결-해보기">3.2 BFS 알고리즘으로 해결 해보기</h2>
<p>BFS 알고리즘은 지난 포스팅에서 소개한 바 있다. <a href="https://velog.io/@noh_level0/%EA%B7%B8%EB%9E%98%ED%94%84-%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98BFSDFS">BFS 알고리즘</a></p>
<p>따라서 큐를 사용해 구현해보았다.
이처럼 자료구조를 활용해 구현해보니 보다 간편하고 알아보기 쉬운 코드로 변경되었다.</p>
<pre><code class="language-python">from collections import deque

n = int(input())
k = int(input())

board = [[0]*n for _ in range(n)] #사과의 위치를 담는다.
board[0][0] = 1 #처음 위치는 무조건 방문처리

for _ in range(k):
    a, b = map(int, input().split())
    board[a-1][b-1] = 5 #사과의 위치에 5라는 값을 넣는다.

l = int(input()) #뱀의 방향 변환 횟수
X = []
move = []
for _ in range(l):
    a, b = input().split()
    X.append(int(a)) #x초 후 방향전환
    move.append(b) #&#39;L&#39;은 왼쪽으로 90도 방향전환
                   #&#39;D&#39;는 오른쪽으로 90도 방향전환

dx = [0, -1, 0, 1]
dy = [1, 0, -1, 0]
# 순서대로 오, 위, 왼, 아
direc = 0 #처음 방향은 오른쪽
x, y = 0, 0 #뱀의 머리 좌표
q = deque() #뱀의 위치를 담을 큐
q.append((x, y))
time = 1 #게임시간

def turn(d, m):
    if m == &#39;L&#39;:
        d += 1
        if d &gt; 3:
            d = 0
    elif m == &#39;D&#39;:
        d -= 1
        if d &lt; 0:
            d = 3
    return d

while True:
    # 방향 전환 시
    if (time-1) in X:
        direc = turn(direc, move[X.index(time-1)])

    x = x + dx[direc]
    y = y + dy[direc]

    if x &lt; 0 or x &gt;= n or y &lt; 0 or y &gt;= n:
        break
    if board[x][y] != 5 and board[x][y] == 0:
        # 사과가 없고
        # 자기자신의 몸과 부딪히지 않았다면 방문처리한다.
        time += 1

        board[x][y] = 1
        q.append((x, y))
        tx, ty = q.popleft()
        board[tx][ty] = 0

    elif board[x][y] == 5:
        # 사과가 있다면
        time += 1

        q.append((x, y))
    else:
        break

print(time)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[그래프 탐색 알고리즘(BFS/DFS)]]></title>
            <link>https://velog.io/@noh_level0/%EA%B7%B8%EB%9E%98%ED%94%84-%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98BFSDFS</link>
            <guid>https://velog.io/@noh_level0/%EA%B7%B8%EB%9E%98%ED%94%84-%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98BFSDFS</guid>
            <pubDate>Sun, 14 Apr 2024 01:35:04 GMT</pubDate>
            <description><![CDATA[<h1 id="1-탐색search이란">1. 탐색(Search)이란</h1>
<ul>
<li>많은 양의 데이터 중에서 원하는 데이터를 찾는 과정</li>
<li>대표적인 그래프 탐색 알고리즘으로 <strong>DFS, BFS</strong>를 꼽을 수 있다.</li>
<li><span style="color: red">코딩 테스트의 주요 알고리즘으로 반드시 숙지해야 한다!!</span></li>
</ul>
<h1 id="2-스택stack">2. 스택(Stack)</h1>
<ul>
<li><p>먼저 들어간 데이터가 나중에 나오는 자료구조이다.(LIFO)</p>
</li>
<li><p>DFS 알고리즘에 사용된다.</p>
<ul>
<li><h3 id="스택-삽입-과정">스택 삽입 과정</h3>
<p><img src = "https://velog.velcdn.com/images/noh_level0/post/1770c84a-1e83-4665-992b-757a88a23869/image.png"/></p>
</li>
<li><h3 id="스택-삭제-과정">스택 삭제 과정</h3>
<p><img src = "https://velog.velcdn.com/images/noh_level0/post/36568f08-27e3-4ee2-a127-6fc4a5d5249c/image.png"/></p>

</li>
</ul>
</li>
</ul>
<h3 id="21-재귀-함수">2.1 재귀 함수</h3>
<ul>
<li>컴퓨터가 연속적으로 함수를 호출하게 되면 이는 컴퓨터 메모리 내부의 스택 프레임에 쌓인다.</li>
<li>따라서 스택을 사용해야 할 경우 구현상 스택 대신 재귀 함수를 사용하는 경우가 많다.</li>
<li><strong>DFS를 간결하게 작성하기 위해 재귀 함수를 활용</strong>하여 구현하기도 한다.</li>
</ul>
<h1 id="3-큐queue">3. 큐(Queue)</h1>
<ul>
<li><p>먼저 들어간 데이터가 먼저 나오는 자료구조이다.(FIFO)</p>
</li>
<li><p>BFS 알고리즘에 사용된다.</p>
<ul>
<li><h3 id="큐-삽입-과정">큐 삽입 과정</h3>
<p><img src = "https://velog.velcdn.com/images/noh_level0/post/b1a24eff-5ef6-420f-af6f-49f114c18b25/image.png"/></p>


</li>
</ul>
</li>
</ul>
<ul>
<li><h3 id="큐-삭제-과정">큐 삭제 과정</h3>
<p><img src = "https://velog.velcdn.com/images/noh_level0/post/d65cac3e-da1c-4337-ba36-f0fb718265ee/image.png"/></p>

</li>
</ul>
<ul>
<li>파이썬에서는 큐를 구현하기 위해 deque 라이브러리를 사용한다.<pre><code class="language-python">from collections import deque
</code></pre>
</li>
</ul>
<p>q = deque()</p>
<h1 id="삽입">삽입</h1>
<p>q.append(1)</p>
<h1 id="삭제">삭제</h1>
<p>q.popleft()</p>
<pre><code>
# 4. DFS(Depth First Search)
* 현재 탐색중인 그래프 경로의 가장 깊은 곳까지 탐색한 후 다른 루트로 다시 탐색하는 방식이다.
* 스택 또는 재귀 함수를 활용하여 구현한다.
* 동작 과정
   1. 탐색을 시작할 노드를 스택에 삽입해 방문 처리한다.
   2. 스택 최상단에 있는 노드에 방문하지 않은 인접 노드가 있다면 해당 인접 노드를 스택에 넣고 방문 처리한다. 만약 방문하지 않은 인접 노드가 없다면 스택에서 최상단에 있는 노드를 꺼낸다.
      &lt;span style=&quot;color: darkorange&quot;&gt;* 인접한 노드의 방문 순서 기준은 문제마다 다를 수 있지만, 일단 번호가 낮은 인접 노드부터 방문한다고 가정한다.&lt;/span&gt;
   3. 2번의 과정을 더 이상 수행할 수 없을 때까지 반복한다.

```python
def dfs(graph, v, visited):
    # 현재 노드는 방문으로 처리
    visited[v] = True
    print(v, end=&#39; &#39;)

    # 현재 노드와 연결된 다른 노드를 재귀적으로 방문한다.
    for i in graph[v]:
        if not visited[i]:
            dfs(graph, i, visited)

graph = [
    [],
    [2, 3, 8],
    [1, 7],
    [1, 4, 5],
    [3, 5],
    [3, 4],
    [7],
    [2, 6, 8],
    [1, 7]
]

# 각 노드의 방문정보 저장
visited = [False] * 9

dfs(graph, 1, visited)</code></pre><h1 id="5-bfsbreadth-first-search">5. BFS(Breadth First Search)</h1>
<ul>
<li>탐색을 시작하는 노드에서 인접한 노드들을 우선적으로 탐색하는 방식이다.</li>
<li>큐를 활용하여 구현한다.</li>
<li><span style="color: red">BFS는 특정 조건에서의 최단 경로를 구하기 위한 목적으로도 효과적으로 사용이 가능하다.</span></li>
<li>동작 과정<ol>
<li>탐색을 시작할 노드를 큐에 삽입해 방문 처리한다.</li>
<li>큐에서 노드를 꺼내고 해당 노드의 인접 노드 중 방문하지 않은 노드들을 모두 큐에 삽입한 후 방문 처리한다.</li>
<li>2번의 과정을 더 이상 수행할 수 없을 때까지 반복한다.</li>
</ol>
</li>
</ul>
<pre><code class="language-python">from collections import deque

def bfs(graph, start, visited):
    queue = deque([start])
    # 현재 노드를 방문 처리
    visited[start] = True

    while queue:
        v = queue.popleft()
        print(v, end=&quot; &quot;)
        # 해당 원소와 연결되어 있고 아직 방문하지 않은 원소라면 큐에 삽입
        for i in graph[v]:
            if not visited[i]:
                queue.append(i)
                visited[i] = True

graph = [
    [],
    [2, 3, 8],
    [1, 7],
    [1, 4, 5],
    [3, 5],
    [3, 4],
    [7],
    [2, 6, 8],
    [1, 7]
]

visited = [False] * 9

bfs(graph, 1, visited)</code></pre>
<p><br/><br/><br/></p>
<blockquote>
<p>그림 폰트 출처: 제주특별자치도, 제주서체, <a href="https://www.jeju.go.kr/jeju/symbol/font/infor.htm">https://www.jeju.go.kr/jeju/symbol/font/infor.htm</a>
참고 서적: 이것이 취업을 위한 코딩 테스트다 with 파이썬, 나동빈, 한빛미디어
참고 강의: <a href="https://www.youtube.com/watch?v=7C9RgOcvkvo&amp;list=PLRx0vPvlEmdAghTr5mXQxGpHjWqSz0dgC&amp;index=3">나동빈님 유튜브 강의</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BAEKJOON] 2750 수 정렬하기 - 파이썬]]></title>
            <link>https://velog.io/@noh_level0/BAEKJOON-2750-%EC%88%98-%EC%A0%95%EB%A0%AC%ED%95%98%EA%B8%B0-%ED%8C%8C%EC%9D%B4%EC%8D%AC</link>
            <guid>https://velog.io/@noh_level0/BAEKJOON-2750-%EC%88%98-%EC%A0%95%EB%A0%AC%ED%95%98%EA%B8%B0-%ED%8C%8C%EC%9D%B4%EC%8D%AC</guid>
            <pubDate>Mon, 29 Jan 2024 19:10:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="백준-2750-문제-링크"><a href="https://www.acmicpc.net/problem/2750">백준 2750 문제 링크</a></h4>
</blockquote>
<h1 id="💡-1-문제-정의">💡 1. 문제 정의</h1>
<p>이 문제는 주어진 숫자를 오름차순으로 정렬하는 문제이다.
파이썬에서 제공하는 내장 메서드인 sort()를 사용하면 쉽게 해결이 가능하며,
주어진 숫자의 범위가 1 ~ 1000 으로 매우 작기 때문에 $$O(n^2)$$의 시간복잡도로도 해결이 가능하다.
<br/></p>
<h1 id="🤔-2-해결-방법">🤔 2. 해결 방법</h1>
<p>내장 메서드 sort()를 사용해 해결$$^1$$, 시간 복잡도가 $$O(n^2)$$인 버블정렬$$^2$$ 이 2가지 방법으로 문제를 해결해본다.
<br/></p>
<h1 id="📑-3-문제-풀이">📑 3. 문제 풀이</h1>
<h2 id="31-파이썬-내장함수-sort를-사용해보기">3.1 파이썬 내장함수 sort()를 사용해보기</h2>
<pre><code class="language-python">arr = []

N = int(input())

for i in range(N):
    a = int(input())
    arr.append(a)

arr.sort()
for i in arr : print(i)</code></pre>
<br/>

<h2 id="32-버블정렬-해보기">3.2 버블정렬 해보기</h2>
<h3 id="321-버블정렬이란">3.2.1 버블정렬이란?</h3>
<p><img src = "https://velog.velcdn.com/images/noh_level0/post/3522df17-4914-4436-8cd0-e0ee3e3de143/image.png"/></p>

<p>버블정렬은 그림과 같이 진행된다.</p>
<ol>
<li>양옆의 원소를 비교한다.</li>
<li>왼쪽의 숫자가 더 크다면 오른쪽의 숫자와 자리를 바꾼다.</li>
<li>1라운드가 끝나면 가장 오른쪽에 있는 숫자는 가장 큰 숫자가 되므로 다음 라운드에서는 이 숫자를 제외하고 숫자 비교를 반복한다.</li>
</ol>
<p>따라서 버블정렬을 사용해 정렬을 하는 코드는 다음과 같다.</p>
<pre><code class="language-python">arr = []

N = int(input())

for i in range(N):
    a = int(input())
    arr.append(a)

for x in range(N-1): #총 라운드 횟수만큼
    for y in range(N-x-1): #한 라운드 내에 비교하는 횟수
        if arr[y] &gt; arr[y+1]:
            temp = arr[y+1]
            arr[y+1] = arr[y]
            arr[y] = temp

for i in arr : print(i)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 5. 스프링 빈과 의존 관계]]></title>
            <link>https://velog.io/@noh_level0/Spring-Boot-5.-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88%EA%B3%BC-%EC%9D%98%EC%A1%B4-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@noh_level0/Spring-Boot-5.-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88%EA%B3%BC-%EC%9D%98%EC%A1%B4-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Wed, 24 Jan 2024 11:03:16 GMT</pubDate>
            <description><![CDATA[<h1 id="1-컴포넌트-스캔과-자동-의존-관계-설정">1. 컴포넌트 스캔과 자동 의존 관계 설정</h1>
<p>일단 코드를 보며 이해해 보자.</p>
<pre><code class="language-java">package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {

    //private final MemberService memberService = new MemberService();

    private final MemberService memberService;
    //@Autowired private MemberService memberService; //필드 주입

//    private MemberService memberService;
//    @Autowired
//    public void setMemberService(MemberService memberService){
//        this.memberService = memberService;
//    }                                                 setter 주입

    @Autowired
    public MemberController(MemberService memberService) { //생성자로 스프링 컨테이너에 등록(생성자 주입)
        this.memberService = memberService;
    }
    /**
     * MemberService.java에 @Service를 달아주지 않으면
     * &#39;hello.hellospring.service.MemberService&#39; that could not be found. 오류가 뜬다.
     * **/
}</code></pre>
<p>코드의 의미를 하나씩 뜯어보도록 한다.</p>
<h3 id="11-controller">1.1 @Controller</h3>
<p>이 어노테이션이 있으면 스프링이 시작될 때 스프링 컨테이너에 <strong><em>MemberController 객체</em></strong>를 생성해서 스프링에 넣어두고 _<strong>스프링이 관리</strong>_한다. 
이를 스프링 컨테이너에서 스프링 빈이 관리된다고 표현한다.</p>
<h3 id="12-private-final-memberservice-memberservice--new-memberservice">1.2 private final MemberService memberService = new MemberService();</h3>
<p>컨트롤러에서는 MemberService 객체를 사용해야 할 필요성이 있다.
이때 _<strong>new를 통해 MemberService 객체를 생성</strong>_하여 사용할 수도 있다.
그러나 현재 이 Controller가 아닌 다른 여러 Controller에서도 MemberService 객체를 사용할 수 있는데, MemberService는 큰 기능을 가지지 않으며 여러개를 생성할 필요성도 없으므로 _<strong>하나만 생성하여 공용으로 사용</strong>_하는 것이 좋다. _<strong>따라서 스프링 컨테이너에 등록하여 사용</strong>_하는 것이 좋다.(스프링 컨테이너에 등록하게 되면 딱 하나만 등록된다.)</p>
<h3 id="13-autowired">1.3 @Autowired</h3>
<p>@Autowired 어노테이션을 통해 memberService을 스프링이 스프링 컨테이너에 있는 MemberService 객체와 연결해준다.
즉, @Autowired를 쓰면 Cotroller와 Service가 연결되며, MemberController가 생성될 때 스프링 빈에 등록되어 있는 MemberService 객체를 가져와 넣어준다.
<em><strong>이것이 바로 Dependency Injection이다.(의존관계 주입)</strong></em></p>
<blockquote>
<p>💡 MemberService.java에 <strong>@Service</strong>를 달아주지 않으면 <span style="color: red">&#39;hello.hellospring.service.MemberService&#39; that could not be found.</span> 오류가 뜬다.
즉, <strong>@Service</strong> 어노테이션을 사용해 스프링이 관리해야하는 객체라는 것을 알 수 있도록 해준다. 이를 통해 스프링이 시작될 때 스프링이 <strong>스프링 컨테이너에 MemberService를 등록</strong>해준다.</p>
</blockquote>
<h3 id="14-스프링-빈을-등록하는-2가지-방법">1.4 스프링 빈을 등록하는 2가지 방법</h3>
<ol>
<li>컴포넌트 스캔과 자동 의존관계 설정</li>
<li>1 @Controller, @Service, @Repository는 컴포넌트 스캔 방식</li>
<li>2 @Autowired는 자동 의존관계 설정</li>
<li>자바 코드로 직접 스프링 빈 등록하기</li>
</ol>
<p>스프링은 스프링 컨테이너에 스프링 빈을 등록할 때 기본적으로 싱글톤으로 등록한다.(_<strong>유일한 하나만 등록하여 공유</strong>_한다.)
즉, _<strong>같은 스프링 빈이면 모두 같은 인스턴스</strong>_이다. <strong>메모리 절약</strong>의 이점도 있음!
설정을 해준다면 싱글톤이 아니게 할 수 있지만, 특별한 경우가 아니라면 대부분 싱글톤을 사용한다.</p>
<blockquote>
<p>💡<span style="color: red">주의!!</span>
HelloSpringApplication의 main으로부터 스프링을 동작시키고 있다.
따라서 hello.hellospring 패키지부터 시작하여 이 하위의 파일은 스프링이 스캔하여 스프링 빈으로 등록을 하게되는데, <strong>hello.hellospring과 동일하거나 하위 패키지가 아니라면 스프링 빈으로 컴포넌트 스캔을 하지 않는다.</strong></p>
</blockquote>
<br/>

<h1 id="2-자바-코드로-직접-스프링-빈-등록하기">2. 자바 코드로 직접 스프링 빈 등록하기</h1>
<p>일단 회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 어노테이션을 제거 후 진행한다.</p>
<pre><code class="language-java">package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

//@Service
public class MemberService {

    private final MemberRepository memberRepository;

    //@Autowired
    public MemberService(MemberRepository memberRepository){ //생성자 사용, 이러한 것을 Dependency Injection이라고 한다.
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     * **/
    public Long join(Member member){
        // 같은 이름이 있는 중복 회원X
        validateDuplicateMember(member); // 중복 회원 검증

        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -&gt; {
                    throw new IllegalStateException(&quot;이미 존재하는 회원입니다.&quot;);
                });
    }

    /**
     * 전체 회원 조회
     * **/
    public List&lt;Member&gt; findMembers(){
        return memberRepository.findAll();
    }

    public Optional&lt;Member&gt; findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}</code></pre>
<pre><code class="language-java">package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.stereotype.Repository;

import java.util.*;

//@Repository
public class MemoryMemberRepository implements MemberRepository{

    private static Map&lt;Long, Member&gt; store = new HashMap&lt;&gt;();
    private static long sequence = 0L; // 0, 1, 2와 같은 key값을 생성해 주는 것.

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional&lt;Member&gt; findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional&lt;Member&gt; findByName(String name) {
        return store.values().stream()
                .filter(member -&gt; member.getName().equals(name))
                .findAny();
    }

    @Override
    public List&lt;Member&gt; findAll() {
        return new ArrayList&lt;&gt;(store.values()); // store에 있는 Member들이 반환됨.
    }

    public void clearStore(){
        store.clear();
    }
}</code></pre>
<p>이러한 상황에서 hello.hellospring 내에 SpringConfig.java 파일을 하나 생성 후 아래와 같이 작성한다.</p>
<pre><code class="language-java">package hello.hellospring;


import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
    /**
     * 자바 코드로 직접 스프링 빈 등록하기!!
     * **/

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}</code></pre>
<p>이와 같이 작성하면 Spring이 시작될 때 <strong>@Configuration</strong>을 본 후 <strong>@Bean</strong>을 통해 스프링 빈에 등록해야 한다라는 인식을 하게된다.
따라서 MemberService, MemberRepository를 스프링 빈에 등록해준다.</p>
<p>이후 MemberService.java에서 스프링 빈에 등록되어 있는 MemberRepository를 생성자를 통해 넣어준다. 지금과 같은 경우에는 구현체로 있는 MemoryMemberRepository를 주입해준다. 즉, <strong>Service와 Repository가 연결</strong>된다.</p>
<blockquote>
<p>💡 <strong>Controller에서는</strong> @Controller 어노테이션이 없이 직접 빈으로 등록하는 것이 불가능하다고 한다.(어차피 스프링이 관리해야 하기때문) 
따라서 <strong>@Autowired</strong>를 사용하여 <strong>직접 빈으로 등록한 MemberService 객체</strong>를 넣어줄 수 있다.</p>
</blockquote>
<br/>

<h1 id="3-didependency-injection의-3가지-방법">3. DI(Dependency Injection)의 3가지 방법</h1>
<p>DI를 할 수 있는 방법으로는 다음과 같은 3가지 방법이 있다.</p>
<ul>
<li>필드 주입</li>
<li>setter 주입</li>
<li>생성자 주입</li>
</ul>
<p>실행 도중 의존 관계가 동적으로 변경되는 경우는 거의 없다. 따라서 생성자 주입을 권장한다고 한다.
코드를 통해 해당 3가지 방법을 살펴본다.</p>
<pre><code class="language-java">package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {

    //private final MemberService memberService = new MemberService();

    private final MemberService memberService;
    //@Autowired private MemberService memberService; //필드 주입

//    private MemberService memberService;
//    @Autowired
//    public void setMemberService(MemberService memberService){
//        this.memberService = memberService;
//    }                                                 setter 주입

    @Autowired
    public MemberController(MemberService memberService) { //생성자로 스프링 컨테이너에 등록(생성자 주입)
        this.memberService = memberService;
    }
    /**
     * MemberService.java에 @Service를 달아주지 않으면
     * &#39;hello.hellospring.service.MemberService&#39; that could not be found. 오류가 뜬다.
     * **/
}</code></pre>
<h3 id="31-필드-주입">3.1 필드 주입</h3>
<pre><code class="language-java">@Autowired private MemberService memberService;</code></pre>
<p>이와 같은 방법을 필드 주입이라고 하며, <strong>final 키워드를 사용할 수 없다.</strong> final 사용 시 선언과 동시에 초기화를 해야한다. 그러나 객체의 생성 즉, 생성자 이후 호출되므로 final 키워드 사용이 불가능하다.</p>
<h3 id="32-setter-주입">3.2 setter 주입</h3>
<pre><code class="language-java">private MemberService memberService;

@Autowired
public void setMemberService(MemberService memberService){
    this.memberService = memberService;
}</code></pre>
<p>이와 같은 방법을 setter 주입이라고 하며, <strong>final 키워드를 사용할 수 없다.</strong> 마찬가지로 final 사용 시 선언과 동시에 초기화를 해야한다. 하지만 객체의 생성 즉, 생성자 이후 호출되므로 final 키워드 사용이 불가능하다.</p>
<p><strong>setter주입의 단점</strong>
누군가가 MemberController를 호출했을 때 해당 setter 메서드가 public으로 열려있어야 한다. 그러나 setMemberService를 중간에 바꾸는 일은 거의 없다. 그러므로 public으로 노출이 되는 문제가 있다.(중간에 잘못 바꾸면 문제가 발생할 수 있음.)</p>
<h3 id="33-생성자-주입">3.3 생성자 주입</h3>
<pre><code class="language-java">private final MemberService memberService;

@Autowired
    public MemberController(MemberService memberService) { //생성자로 스프링 컨테이너에 등록(생성자 주입)
        this.memberService = memberService;
    }</code></pre>
<p>이와 같은 방법을 생성자 주입이라고 하며, <strong>final 키워드를 사용할 수 있다는 장점이 있다!!</strong></p>
<br/>

<h1 id="4-참고-및-주의사항">4. 참고 및 주의사항</h1>
<p>실무에서는 주로 정형화되어있는 컨트롤러, 서비스, 리포지토리와 같은 코드에서는 컴포넌트 스캔을 사용한다고 한다.
만일 정형화되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다고 한다.</p>
<p><strong>ex)</strong> DB를 현재 지정하지 않은 상황인데, 차후 DB를 선정했다고 가정하면</p>
<pre><code class="language-java">@Bean
public MemberRepository memberRepository(){
    return new MemoryMemberRepository();
}</code></pre>
<p>에서</p>
<pre><code class="language-java">@Bean
public MemberRepository memberRepository(){
    return new DbMemberRepository();
}</code></pre>
<p>로만 변경하면 나머지를 손댈 필요가 없어진다.(구현체를 바꿔준다.)</p>
<blockquote>
<p>❗주의❗
<strong>@Autowired</strong>를 통한 DI는 helloController, MemberService처럼 스프링이 관리하는 객체에서만 동작한다.
만일 스프링 빈으로 등록하지 않고, 직접 생성한 객체에서라면 동작하지 않는다.(스프링 컨테이너에 올라가야 <strong>@Autowired</strong> 기능이 동작한다.)</p>
</blockquote>
<p><strong>ex)</strong> MemberService를 스프링 빈으로 등록하지 않은 상태에서 해당 클래스 내에 <strong>@Autowired를 사용하더라도 동작하지 않는다.</strong>(스프링이 MemberService를 관리하지 않기 때문)</p>
<blockquote>
</blockquote>
<p><span style="color: red">또한</span></p>
<pre><code class="language-java">public static void main(String[] args){
    MemberService memberService = new MemberService();
}</code></pre>
<p>이와 같이 new를 통해 새로운 객체를 생성하는 경우에도 <strong>@Autowired를 사용하더라도 동작하지 않는다.</strong></p>
<p><br/><br/><br/></p>
<blockquote>
<p>본 포스팅은 inflearn 김영한 강사님의 &quot;스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술&quot; 강의에 기반하여 작성되었습니다. 
<a href="https://inf.run/hivx6">강의 링크</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 4. 회원 관리 예제1]]></title>
            <link>https://velog.io/@noh_level0/Spring-Boot-4.-%ED%9A%8C%EC%9B%90-%EA%B4%80%EB%A6%AC-%EC%98%88%EC%A0%9C1</link>
            <guid>https://velog.io/@noh_level0/Spring-Boot-4.-%ED%9A%8C%EC%9B%90-%EA%B4%80%EB%A6%AC-%EC%98%88%EC%A0%9C1</guid>
            <pubDate>Tue, 23 Jan 2024 14:21:28 GMT</pubDate>
            <description><![CDATA[<h1 id="1-요구사항">1. 요구사항</h1>
<p><strong>회원의 데이터:</strong> 회원ID, 이름
<strong>기능:</strong> 회원 등록 및 조회
일단 DB를 선택하지 않은 상황이라고 <strong>가정</strong>한다.</p>
<blockquote>
<p>💡 <strong>회원ID는 시스템에서 설정하도록 한다.</strong></p>
</blockquote>
<p><strong>컨트롤러:</strong> MVC에서의 컨트롤러
<strong>서비스:</strong> 비즈니스 로직을 구현
<strong>리포지토리:</strong> DB접근, 도메인 객체를 DB에 저장 및 관리
<strong>도메인:</strong> 비즈니스 도메인 객체. ex) 회원, 주문 등 주로 DB에 저장되고 관리되는 것</p>
<h1 id="2-구현">2. 구현</h1>
<p>전체 프로젝트 폴더 구조는 다음과 같이 구성된다.</p>
<p>
<img src = "https://velog.velcdn.com/images/noh_level0/post/2e0fa270-42a5-4d16-926e-ed675eae69b0/image.png" width="350" height="350"/></p>

<ul>
<li><h3 id="회원-도메인-클래스">회원 도메인 클래스</h3>
<p>
<img src = "https://velog.velcdn.com/images/noh_level0/post/9090913d-2f75-4cbe-b96d-1fff791daeb3/image.png" width=90%/>
</p>
각 회원은 id와 name을 갖도록 하여 저장한다. 이때 id는 고객이 지정하는 것이 아닌 시스템이 지정하도록 한다.(Key 값)
<br/><br/></li>
<li><h3 id="회원-리포지토리-클래스">회원 리포지토리 클래스</h3>
<p>DB를 선정하지 않았으므로 리포지토리는 우선 interface를 만들고, 구현체는 메모리 구현체로 만들도록 한다. (나중에 DB가 선정이 된다면 구현 클래스를 바꿔주어야 하므로 interface가 필요하다.)</p>
<ul>
<li><strong>인터페이스</strong><img src = "https://velog.velcdn.com/images/noh_level0/post/94e151c1-bc43-4b5d-8d2a-cbe63d148325/image.png" width=90%/></li>
<li><strong>구현체</strong><img src = "https://velog.velcdn.com/images/noh_level0/post/7ae384bd-d110-4d58-ac7d-7c84b708fb9b/image.png" width=90%/>
> * _**구현체 기능**_ 
>   1. 회원 등록
2. id로 조회
3. 이름으로 조회
4. 전체 회원 조회
5. 전체 회원 삭제  
</li>
</ul>
</li>
<li><h3 id="회원-리포지토리-테스트">회원 리포지토리 테스트</h3>
<p>개발한 기능을 테스트하는 방법은 여러가지가 존재한다.
이러한 방법 중에는 main 메서드로 실행을 하거나 컨트롤러를 통해 해당 기능을 실행하는 방법이 있다.
그러나 이 방법은 여러 단점이 존재하는데, <strong>1. 준비하고 실행하는 것에 있어 오래 걸리는 문제 2. 반복 실행하기 어렵다는 문제 3. 여러 테스트를 한꺼번에 실행하기 어렵다</strong>는 단점이 있다. 따라서 자바의 <strong>JUnit</strong>이라는 프레임워크로 테스트를 진행하는 것을 통해 단점을 해결할 수 있다.</p>
<pre><code class="language-java">package hello.hellospring.repository;
</code></pre>
</li>
</ul>
<p>import hello.hellospring.domain.Member;
//import org.junit.jupiter.api.Assertions;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;</p>
<p>import java.util.List;</p>
<p>import static org.assertj.core.api.Assertions.*;</p>
<p>class MemoryMemberRepositoryTest {</p>
<pre><code>MemoryMemberRepository repository = new MemoryMemberRepository();

@AfterEach // 하나의 메서드가 끝날 때마다 호출되는 콜백 메서드
public void afterEach(){
    repository.clearStore();
}

@Test
public void save(){
    Member member = new Member();
    member.setName(&quot;spring&quot;);

    repository.save(member); //이때 자동으로 id가 세팅됨.

    Member result = repository.findById(member.getId()).get();
    //System.out.println(&quot;result = &quot; + (member == result));

    //Assertions.assertEquals(member, result); // 순서는 expected, actual

    //Assertions.assertThat(member).isEqualTo(result);

    assertThat(member).isEqualTo(result); // Assersions를 static import를 했을 경우
}

@Test
public void findByName(){
    Member member1 = new Member();
    member1.setName(&quot;spring1&quot;);
    repository.save(member1);

    Member member2 = new Member();
    member2.setName(&quot;spring2&quot;);
    repository.save(member2);

    Member result = repository.findByName(&quot;spring1&quot;).get();
    assertThat(result).isEqualTo(member1);
}

@Test
public void findAll(){
    Member member1 = new Member();
    member1.setName(&quot;spring1&quot;);
    repository.save(member1);

    Member member2 = new Member();
    member2.setName(&quot;spring2&quot;);
    repository.save(member2);

    List&lt;Member&gt; result = repository.findAll();

    assertThat(result.size()).isEqualTo(2);
}</code></pre><p>}</p>
<pre><code>
테스트는 서로 순서와 의존 관계가 없이 실행되어야 한다. 따라서 하나의 테스트가 끝날 때마다 저장소나 공용 데이터를 지워주어야 문제가 생기지 않는다. 
그러므로 **@AfterEach**를 사용해 하나의 메서드가 끝날 때마다 호출되는 콜백 메서드 **afterEach()**에서 데이터를 모두 지워준다. 
이러한 과정이 없다면 테스트 과정에서 오류가 발생하게 된다.
&lt;br/&gt;&lt;br/&gt;

* ### 회원 서비스 클래스
```java
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {

    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    //이렇게 하게되면 MemberServiceTest와 다른 memberRepository 객체를 생성하여 테스트를 하게 되는 문제가 발생함.
    //따라서 아래와 같이 수정
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository){ //생성자 사용, 이러한 것을 Dependency Injection이라고 한다.
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     * **/
    public Long join(Member member){
        // 같은 이름이 있는 중복 회원X
        validateDuplicateMember(member); // 중복 회원 검증

        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -&gt; {
                    throw new IllegalStateException(&quot;이미 존재하는 회원입니다.&quot;);
                });
    }

    /**
     * 전체 회원 조회
     * **/
    public List&lt;Member&gt; findMembers(){
        return memberRepository.findAll();
    }

    public Optional&lt;Member&gt; findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}</code></pre><p>같은 이름이 있는 중복 회원은 만들 수 없는 것으로 초기에 가정하였다. 따라서 중복 회원인지 검증을 통해 중복 회원이라면 예외를 발생시키는 것으로 한다.
또한 클래스 내에서 <strong>MemberRepository의 객체</strong>를 새로 생성하게 되면 차후 작성할 회원 서비스 테스트 클래스(MemberServiceTest class)와 <strong>다른 객체</strong>를 생성하여 테스트하게 되므로 생성자를 통해 매개변수를 받아 객체를 생성하도록 한다. 이는 <strong>Dependency Injection</strong>이라고 한다.</p>
<ul>
<li><h3 id="회원-서비스-테스트">회원 서비스 테스트</h3>
<pre><code class="language-java">package hello.hellospring.service;
</code></pre>
</li>
</ul>
<p>import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;</p>
<p>import java.util.Optional;</p>
<p>import static org.assertj.core.api.Assertions.<em>;
import static org.junit.jupiter.api.Assertions.</em>;</p>
<p>class MemberServiceTest {</p>
<pre><code>//MemberService memberService = new MemberService();
//MemoryMemberRepository MemberRepository = new MemoryMemberRepository();
//MemberService에서 만든 객체와 다른 객체를 생성하여 테스트하는 것이므로 문제가 있음
//수정
MemberService memberService;
MemoryMemberRepository memberRepository;

@BeforeEach
public void beforeEach(){
    memberRepository = new MemoryMemberRepository();
    memberService = new MemberService(memberRepository);
}

@AfterEach // 하나의 메서드가 끝날 때마다 호출되는 콜백 메서드
public void afterEach(){
    memberRepository.clearStore();
}

@Test
void 회원가입() {
    //given
    Member member = new Member();
    member.setName(&quot;hello&quot;);

    //when
    Long saveId = memberService.join(member);

    //then
    Member findMember = memberService.findOne(saveId).get();
    assertThat(member.getName()).isEqualTo(findMember.getName());
}

@Test
public void 중복_회원_예외(){
    //given
    Member member1 = new Member();
    member1.setName(&quot;spring&quot;);

    Member member2 = new Member();
    member2.setName(&quot;spring&quot;);

    //when
    memberService.join(member1);

    /* 방법1: try, catch 사용
    try {
        memberService.join(member2);
        fail();
    }catch (IllegalStateException e){
        assertThat(e.getMessage()).isEqualTo(&quot;이미 존재하는 회원입니다.&quot;);
    }
    */

    //방법2
    IllegalStateException e = assertThrows(IllegalStateException.class, () -&gt; memberService.join(member2));
    // () -&gt; memberService.join(member2) 로직 실행 시 IllegalStateException 예외가 터져야 한다.
    assertThat(e.getMessage()).isEqualTo(&quot;이미 존재하는 회원입니다.&quot;); //메시지 검증

    //then
}

@Test
void findMembers() {
}

@Test
void findOne() {
}</code></pre><p>}</p>
<p>```
이처럼 <strong>@BeforeEach</strong>를 사용해 테스트를 실행하기 전 MemoryMemberRepository 객체 생성 후 MemberService 객체를 생성하는 것을 통해 동일한 MemberRepository 객체를 생성하도록 한다.
<br/><br/><br/></p>
<blockquote>
<p>본 포스팅은 inflearn 김영한 강사님의 &quot;스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술&quot; 강의에 기반하여 작성되었습니다. 
<a href="https://inf.run/hivx6">강의 링크</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 3. 정적 컨텐츠, MVC와 템플릿 엔진, API]]></title>
            <link>https://velog.io/@noh_level0/Spring-Boot-3.-%EC%A0%95%EC%A0%81-%EC%BB%A8%ED%85%90%EC%B8%A0-MVC%EC%99%80-%ED%85%9C%ED%94%8C%EB%A6%BF-%EC%97%94%EC%A7%84-API</link>
            <guid>https://velog.io/@noh_level0/Spring-Boot-3.-%EC%A0%95%EC%A0%81-%EC%BB%A8%ED%85%90%EC%B8%A0-MVC%EC%99%80-%ED%85%9C%ED%94%8C%EB%A6%BF-%EC%97%94%EC%A7%84-API</guid>
            <pubDate>Mon, 22 Jan 2024 16:55:27 GMT</pubDate>
            <description><![CDATA[<h1 id="1-정적-컨텐츠">1. 정적 컨텐츠</h1>
<p>파일을 그대로 웹 브라우저(클라이언트)에 전달하는 방식.
기본적으로 Spring Boot에서는 정적 컨텐츠를 /static <span style="color: red">or</span> /public <span style="color: red">or</span> /resources <span style="color: red">or</span> /META-INF/resources 폴더에서 찾아 제공한다.</p>
<hr>

<p><em><strong>예제</strong></em></p>
<pre><code class="language-html">&lt;!DOCTYPE HTML&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html;charset=UTF-8&quot;&gt;
    &lt;title&gt;static content&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
정적 컨텐츠 입니다.
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>static 폴더에 이와 같이 hello-static.html을 만든다.</p>
<ul>
<li>결과<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/b9020347-2ba3-4657-81c6-6b01dcdbd536/image.png"/ width=80%>
</p>
http://localhost:8080/hello-static.html에 접속하게 되면 이와 같은 정적 컨텐츠가 반환되는 것을 볼 수 있다. 또한 이러한 정적 컨텐츠에서는 어떠한 프로그래밍은 할 수 없다.

</li>
</ul>
<blockquote>
<ul>
<li><strong>정적 컨텐츠가 돌아가는 방식</strong></li>
</ul>
</blockquote>
<ol>
<li>웹 브라우저에서 <a href="http://localhost:8080/hello-static.html%EC%9D%84">http://localhost:8080/hello-static.html을</a> 치면 내장 톰캣 서버가 이 요청을 받는다.</li>
<li>내장 톰캣 서버는 이 요청을 스프링에 넘긴다.</li>
<li>스프링은 hello-static과 관련된 컨트롤러가 있는지 찾아본다.</li>
<li><strong>만약 맵핑된 컨트롤러가 없다면</strong> resources: static 내부에 있는 hello-static.html을 찾는다.</li>
<li>해당 hello-static.html이 있으므로 그것을 웹 브라우저에 반환해준다.</li>
</ol>
<br/>

<h1 id="2-mvc와-템플릿-엔진">2. MVC와 템플릿 엔진</h1>
<p>서버에서 html을 변형하여 웹 브라우저(클라이언트)에 전달하는 방식.</p>
<p><strong>MVC:</strong> Model, View, Controller</p>
<p><strong>Model:</strong> Model에 화면에 필요한 것들을 담아서 화면쪽에 넘겨준다.
<strong>View:</strong> 화면을 그리는 데 모든 영향을 집중한다.
<strong>Controller:</strong> 비즈니스 로직과 서버 뒷단에 관련된 일을 처리한다.</p>
<p>과거에는 View, Controller가 따로 분리되어 있지 않았다고 한다.(View에서 다 처리. 이를 소위 Model1 방식이라고 하며, jsp로 많이 했음.)
현재에는 MVC 방식으로 많이 진행된다.</p>
<hr>

<p><em><strong>예제</strong></em></p>
<pre><code class="language-java">@Controller
public class HelloController {
    @GetMapping(&quot;hello-mvc&quot;)
    public String helloMvc(@RequestParam(&quot;name&quot;) String name, Model model){
        model.addAttribute(&quot;name&quot;, name);
        return &quot;hello-template&quot;;
    }</code></pre>
<p>HelloController.java에 다음과 같이 helloMvc() 메서드를 만든다.
<strong>@RequestParam:</strong> 외부에서 파라미터를 받을 때 사용.
@RequestParam의 required는 true가 디폴트이다. 따라서 무조건 파라미터를 넣어주어야 한다.(현 예제에서는 url을 통해 파라미터를 전달한다.)</p>
<pre><code class="language-html">&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;body&gt;
&lt;p th:text=&quot;&#39;hello &#39; + ${name}&quot;&gt;hello! empty&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>templates 폴더에는 위와 같이 hello-template.html을 작성한다.</p>
<blockquote>
<ul>
<li><strong>thymeleaf의 장점</strong>
thymeleaf의 장점은 html을 그대로 서버없이 열어봐도 껍데기를 볼 수 있다는 것이다. 
즉, 절대경로를 그대로 웹 브라우저에 입력하면 hello! empty가 뜨는 것을 볼 수 있다. 그런데 이 html이 템플릿 엔진으로 동작(서버를 통해 동작)하면 hello! empty가 &#39;hello &#39; + ${name}로 치환된다.</li>
</ul>
</blockquote>
<ul>
<li><p>결과1: 절대경로로 접속</p>
<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/da72d963-8e9f-4a4a-97d2-07d63c224384/image.png"/ width=80%>
</p>
</li>
<li><p>결과2: url로 접속(파라미터로 Level0 전송)</p>
<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/78568506-3ec8-4937-a613-de30711c9ba6/image.png"/ width=80%>
</p>

</li>
</ul>
<blockquote>
<ul>
<li><strong>MVC와 템플릿 엔진의 작동 방식</strong></li>
</ul>
</blockquote>
<ol>
<li>웹 브라우저에서 <a href="http://localhost:8080/hello-mvc%EC%9D%84">http://localhost:8080/hello-mvc을</a> 치면 내장 톰캣 서버가 이 요청을 받는다.</li>
<li>내장 톰캣 서버는 이 요청을 스프링에 넘긴다.</li>
<li>스프링은 HelloController에 hello-mvc라는 메서드가 있는 것을 보고 해당 메서드를 호출해준다.</li>
<li>이 메서드에서 return은 <span style="color: deepskyblue">hello-template</span>, model에는 (name:Level0)를 넣어 스프링에 넘겨준다.</li>
<li>스프링에서 viewResolver(화면 관련 해결자)는 view를 찾아 템플릿 엔진을 연결시켜준다. 따라서 viewResolver가 templates/<span style="color: deepskyblue">hello-template</span>.html을 찾아 Thymeleaf 템플릿 엔진에게 처리를 해달라고 넘겨준다.</li>
<li>Thymeleaf 템플릿 엔진이 렌더링을 하여 변환한 html을 웹 브라우저에 반환한다.<span style="color: red">(정적과 반대)</span></li>
</ol>
<br/>

<h1 id="3-api">3. API</h1>
<p>JSON 포맷으로 웹 브라우저(클라이언트)에 데이터를 전달하는 방식.(통상적으로 요즘의 API 방식이라 함은 이를 의미한다.)</p>
<hr/>

<p><strong><em>예제</em></strong></p>
<pre><code class="language-java">@Controller
public class HelloController {
    @GetMapping(&quot;hello-string&quot;)
    @ResponseBody
    public String helloString(@RequestParam(&quot;name&quot;) String name){
          return &quot;hello &quot;+ name;
      }
}</code></pre>
<p><strong>@ResponseBody:</strong> http의 응답 body에 &quot;hello &quot;+ name 데이터를 직접 넣어주겠다는 의미이다.
ex) <span style="color: red">return &quot;hello &quot;+ name</span>에서 name이 Level0라면 &quot;hello Level0&quot;라는 문자가 요청한 클라이언트에 그대로 내려간다. 
템플릿 엔진과의 차이는 view가 없이 문자가 그대로 내려간다는 것이다.</p>
<ul>
<li>결과, 페이지 소스보기<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/19d2b3ce-0c30-43b2-afcf-7fd14a7ea62e/image.png"/ width=80%>
</p>
<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/26b9ccf2-ae68-43c8-872a-c41c7ff37b18/image.png"/ width=80%>
</p>
문자열이 그대로 전송된 것을 볼 수 있다.

</li>
</ul>
<pre><code class="language-java">@Controller
public class HelloController {
    @GetMapping(&quot;hello-api&quot;)
    @ResponseBody
    public Hello helloApi(@RequestParam(&quot;name&quot;) String name){
        Hello hello = new Hello();
        hello.setName(name);
        return hello; //객체를 넘김
    }

    static class Hello{
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}</code></pre>
<ul>
<li>결과, 페이지 소스 보기<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/b76650f1-69c6-436a-8c0b-55100956705f/image.png"/ width=80%>
</p>
<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/ca18fbad-7ee4-4081-b0f0-7953265c6d22/image.png"/ width=80%>
</p>
JSON 방식으로 전송된 것을 볼 수 있다.

</li>
</ul>
<blockquote>
<ul>
<li><strong>API 작동방식</strong></li>
</ul>
</blockquote>
<ol>
<li>웹 브라우저에서 localhost:8080/hello-api?name=Level0를 입력하여 내장 톰캣 서버에 요청을 넘긴다.</li>
<li>톰캣 내장 서버에서는 이 요청을 스프링에 넘긴다.</li>
<li>스프링은 <strong>@ResponseBody 애노테이션</strong>이 붙어있으므로 HttpMessageConverter가 동작하며, 단순 문자라면 StringConverter가 객체라면 JsonConverter가 기본으로 동작한다.</li>
<li>데이터의 스타일이 바뀌어 http 응답으로 반환된다.(단순 문자라면 http 응답에 그대로 이 데이터를 넘기는 것으로 동작, 객체라면 JSON 방식으로 데이터 스타일을 바꾸어 동작)<blockquote>
<p><em><strong>@ResponseBody란?</strong></em>
http의 body에 문자 내용을 직접 반환하겠다는 의미이다.
<span style="color: red">viewResolver 대신에</span> HttpMessageConverter가 동작한다.
<em>기본 문자처리:</em> StringHttpMessageConverter
<em>기본 객체처리:</em> MappingJackson2HttpMessageConverter
byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있다.</p>
</blockquote>
</li>
</ol>
<p><strong>@ResponseBody를 하고 객체를 반환하는 것은 JSON으로 반환하는 것이 기본으로 세팅되어 있다.</strong></p>
<p><br/><br/><br/></p>
<blockquote>
<p>본 포스팅은 inflearn 김영한 강사님의 &quot;스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술&quot; 강의에 기반하여 작성되었습니다. 
<a href="https://inf.run/hivx6">강의 링크</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 2. View 설정 및 빌드]]></title>
            <link>https://velog.io/@noh_level0/Spring-Boot-2.-View-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EB%B9%8C%EB%93%9C</link>
            <guid>https://velog.io/@noh_level0/Spring-Boot-2.-View-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EB%B9%8C%EB%93%9C</guid>
            <pubDate>Sun, 21 Jan 2024 19:27:41 GMT</pubDate>
            <description><![CDATA[<h1 id="1-welcome-page">1. Welcome Page</h1>
<p>Spring Boot는 resources/static 또는 resources/templates 폴더에 index.html을 만들면 이 페이지를 Welcome 페이지로 만들어준다.</p>
<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/d87abdc7-079b-4d26-a15b-4e9cdaf3b051/image.png" width="40%" height="40%"/>
</p>
이와 같이 resources/static 폴더에 index.html을 생성 후

<pre><code class="language-html:index.html">&lt;!DOCTYPE HTML&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html;charset=UTF-8&quot;&gt;
    &lt;title&gt;Hello&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
정적 컨텐츠
&lt;a href=&quot;/hello&quot;&gt;hello&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>index.html을 이처럼 작성하면</p>
<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/a1dc8c73-d71d-4829-9392-6c5029c74704/image.png" width=90%></img>
</p>
http://localhost:8080/에 접속 시 이러한 화면이 보인다.<br>
문자열만 작성하는 것은 정적 페이지라는 것으로 그대로 웹 브라우저에 넘어가게 된다.(프로그래밍 x)<br/>
<br/>

<h1 id="2-thymeleaf-템플릿-엔진-사용해보기">2. thymeleaf 템플릿 엔진 사용해보기</h1>
<p>템플릿 엔진을 쓰게 된다면 html에서 원하는 부분의 모양을 바꿀 수 있다. 즉, 동적 페이지를 만들 수 있게 된다.</p>
<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/03a10e66-141b-415c-9053-772c1bc62c15/image.png" width=40%></img>
</p>
controller 패키지를 만들고 그 안에 HelloController.java를 생성하고, templates 폴더 안에는 hello.html 파일을 생성한다.

<pre><code class="language-java">@Controller
public class HelloController {

    @GetMapping(&quot;hello&quot;) 
    public String hello(Model model){
        model.addAttribute(&quot;data&quot;, &quot;hello!!&quot;);
        return &quot;hello&quot;;
    }
}</code></pre>
<p>HelloController.java에는 위와 같이 작성한다. <br/></p>
<ul>
<li><strong>@GetMapping(&quot;hello&quot;)</strong>
웹 애플리케이션에서 첫번째 진입점은 컨트롤러이며, @GetMapping(&quot;hello&quot;)라면 웹 어플리케이션에서 <a href="http://localhost:8080/hello%EB%A1%9C">http://localhost:8080/hello로</a> 접속 시 해당 메서드를 호출해준다.<br/></li>
<li><strong>Model</strong>
Spring에서는 model을 만들어 넣어주는데 여기에서는 key는 &quot;data&quot;, value는 &quot;hello!!&quot;가 되어 들어간다.</li>
<li><strong>return</strong>
return &quot;hello&quot;의 의미는 resources/templates에 있는 hello.html로 가서 렌더링해라(화면을 실행시켜라)의 의미가 된다. 이는 기본적으로 Spring Boot에서 세팅된 것으로, 컨트롤러에서 return 값으로 문자를 반환하면 뷰 리졸버(&#39;viewResolver&#39;)가 화면을 찾아서 처리한다. <br/> Spring Boot 템플릿엔진은 기본적으로 viewName 매핑이 된다.
즉, &#39;resources:templates/&#39; + {ViewName} + &#39;.html&#39;</li>
</ul>
<pre><code class="language-html">&lt;!DOCTYPE HTML&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html;charset=UTF-8&quot;&gt;
    &lt;title&gt;Hello&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;p th:text=&quot;&#39;안녕하세요. &#39;+${data}&quot;&gt;안녕하세요. 손님&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>hello.html에는 위와 같이 작성한다. <br/></p>
<p><strong>th:</strong> thymeleaf의 th를 의미
여기에서 {data}는 HelloController.java에서 model.addAttribute에서 key로 넣었던 &quot;data&quot;를 의미하며, 이 코드에서 Value는 &quot;hello!!&quot;이다. 따라서 ${data} 이 부분이 hello!!로 바뀌어 웹 브라우저에 띄워진다.</p>
<p>
<img src="https://velog.velcdn.com/images/noh_level0/post/3f361856-e3d1-44cb-9045-d339654f1af6/image.png" width=80%></img>
</p>
프로젝트 실행 후 http://localhost:8080/hello로 접속하게 되면 이와 같은 화면이 출력된다.
<br/><br/>

<h1 id="3-빌드-후-실행-파일-만들기">3. 빌드 후 실행 파일 만들기</h1>
<p>IntelliJ IDE가 실행하는 것이 아닌 빌드 파일을 만들어 실행해본다.<br/></p>
<ol>
<li>IntelliJ에서 서버를 중지시킨다.</li>
<li>cmd를 프로젝트 폴더 경로 위치에서 실행한 후 gradlew.bat build를 실행한다. (맥의 경우 ./gradlew build 입력)<p><img src="https://velog.velcdn.com/images/noh_level0/post/f41a4afb-bc8e-4d61-b0e8-e6fcf0b1ed22/image.png" width=80%></img></p>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/ff442e29-57ca-4690-bb60-e4a1dfe118c1/image.png" width=80%></img></p></li>
<li>build/libs 폴더로 이동 후 hello-spring-0.0.1-SNAPSHOT.jar 파일을 실행한다.<p><img src="https://velog.velcdn.com/images/noh_level0/post/a2dbc882-ff5a-4443-bf20-9b0f527acff0/image.png" width=80%></img></p>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/d1d1ab84-ee10-4035-84ae-85bb21832116/image.png" width=80%></img></p></li>
<li><a href="http://localhost:8080/">http://localhost:8080/</a> 접속 확인<p><img src="https://velog.velcdn.com/images/noh_level0/post/4a11cb52-aa5e-4ee8-9e40-92c28a61ccf1/image.png" width=80%></img></p>
페이지가 정상적으로 띄워지는 것을 볼 수 있다.

</li>
</ol>
<p><em><strong>따라서 서버 배포 시 hello-spring-0.0.1-SNAPSHOT.jar 파일만 서버에 넣어준 후 실행하면 된다.</strong></em>
<br/></p>
<h1 id="4-빌드가-잘-안-되었을-때">4. 빌드가 잘 안 되었을 때</h1>
<p>gradlew clean build를 입력해 빌드를 다시 시작한다.</p>
<p><img src="https://velog.velcdn.com/images/noh_level0/post/39476349-4c40-4a11-88da-bf109c88507d/image.png" width=80%></img></p>
<span style="color:red">!!</span> gradlew clean 입력 시 bulid 폴더 자체가 <span style="color:red">삭제</span>된다.
<br/><br/><br/>

<blockquote>
<p>본 포스팅은 inflearn 김영한 강사님의 &quot;스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술&quot; 강의에 기반하여 작성되었습니다. 
<a href="https://inf.run/hivx6">강의 링크</a><br>
썸네일 생성: <a href="https://velog.io/@oneook/%EC%8D%B8%EB%84%A4%EC%9D%BC-%EB%A9%94%EC%9D%B4%EC%BB%A4Thumbnail-Maker-Toy-Project">Thumbnail Maker</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 1. 개발 환경 구축 및 프로젝트 생성]]></title>
            <link>https://velog.io/@noh_level0/Spring-Boot-1.-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@noh_level0/Spring-Boot-1.-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Sun, 21 Jan 2024 17:31:53 GMT</pubDate>
            <description><![CDATA[<h1 id="1-jdk-17-설치">1. JDK 17 설치</h1>
<p>ORACLE 홈페이지에서 자신의 운영체제에 맞는 JDK 17을 다운하여 설치한다.
<a href="https://www.oracle.com/kr/java/technologies/downloads/#java17">https://www.oracle.com/kr/java/technologies/downloads/#java17</a>
<br></p>
<h1 id="2-환경변수window-라면-설정">2. 환경변수(Window 라면) 설정</h1>
<ul>
<li><h4 id="21-제어판--시스템-및-보안--시스템--고급-시스템-설정">2.1 제어판 &gt; 시스템 및 보안 &gt; 시스템 &gt; 고급 시스템 설정</h4>
<p align="center">
<img src="https://velog.velcdn.com/images/noh_level0/post/2a970dca-e6c9-4131-bd9e-e26e8e96c8d8/image.png" width="60%">
</p>
</li>
<li><h4 id="22-시스템-변수-추가">2.2 시스템 변수 추가</h4>
<p align="center">
<img src="https://velog.velcdn.com/images/noh_level0/post/175634dc-0c83-4c03-8eb8-72d6517d53de/image.png" width="80%">
</p>
JAVA_HOME이라는 이름으로 시스템 변수를 추가한다. 이때 변수 값은 jdk 17이 설치된 폴더의 경로로 설정한다. (기존에 JAVA_HOME이 있다면 편집으로 경로만 변경해준다.)
</li>
<li><h4 id="23-path-시스템-변수-편집">2.3 Path 시스템 변수 편집</h4>
<p align="center">
<img src="https://velog.velcdn.com/images/noh_level0/post/f763a07f-dc23-4b57-bd3b-49d66959a55a/image.png" width="80%">
</p>
시스템 변수 Path의 편집을 클릭하여 %JAVA_HOME%\bin을 추가해준다.
</li>
<li><h4 id="24-설치-확인">2.4 설치 확인</h4>
<p align="center">
<img src="https://velog.velcdn.com/images/noh_level0/post/925ae96c-e7d2-4d70-b4ad-f87260bde11c/image.png" width="80%">
</p>
cmd에서 java -version을 입력했을 때 java의 버전이 출력된다면 jdk의 설치 및 환경변수 설정이 잘 된 것이다.

</li>
</ul>
<br>

<h1 id="3-ideintellij-설치">3. IDE(IntelliJ) 설치</h1>
<p><a href="https://www.jetbrains.com/ko-kr/idea/download/?section=windows">https://www.jetbrains.com/ko-kr/idea/download/?section=windows</a>
Ultimate 버전과 Community 버전이 있는데, 나는 이 중 무료 버전인 Community 버전을 설치해 주었다.
<br></p>
<h1 id="4-스프링-프로젝트-생성">4. 스프링 프로젝트 생성</h1>
<p><a href="https://start.spring.io/">https://start.spring.io/</a>
위 사이트에서 스프링 프로젝트를 생성한다.</p>
<blockquote>
<ul>
<li>** Project**
<em><strong>Gradle Project</strong></em> 선택
Maven 또는 Gradle은 필요한 라이브러리를 가져오거나 빌드하는 라이프 사이클을 관리하는 툴이다. 과거에는 Maven을 많이 사용했지만 최근에는 Gradle을 많이 사용한다고 한다.</li>
<li><strong>Language</strong>
<em><strong>JAVA</strong></em> 선택</li>
<li><strong>Spring Boot</strong>
<em><strong>3.2.2</strong></em> 선택
SNAPSHOT, M1은 정식 릴리즈 된 버전이 아니므로 정식 릴리즈 된 버전을 선택해준다.</li>
<li><strong>Dependencies</strong>
<em><strong>Spring Web</strong></em>, <em><strong>Thymeleaf</strong></em> 추가</li>
</ul>
</blockquote>
<br>

<h1 id="5-intellij에서-프로젝트-열기">5. IntelliJ에서 프로젝트 열기</h1>
<p align="center">
<img src="https://velog.velcdn.com/images/noh_level0/post/58f92678-cb70-48e6-b372-1fc84e91fffb/image.png" width="80%">
</p>
다운한 Spring 프로젝트의 압축을 푼다.
그 후 IntelliJ를 실행 후 Open > 다운한 프로젝트의 build.gradle을 선택하여 프로젝트를 열어준다.

<h1 id="6-프로젝트-실행하기">6. 프로젝트 실행하기</h1>
<p align="center">
<img src="https://velog.velcdn.com/images/noh_level0/post/ba97d8ee-1e6b-4ca3-b9c7-b7e97aa2a40c/image.png" width="90%">
</p>
HelloSpringApplication.java 파일 위에서 우클릭 후 Run 'HelloSpringApp....main()'을 클릭하면 프로젝트가 실행된다.
<br>

<p>웹 브라우저에서 <a href="http://localhost:8080/">http://localhost:8080/</a> 접속 시 아래와 같은 화면이 뜬다.</p>
<style>
img {
  vertical-align: top or bottom;
}
</style>

<p align="center">
<img src="https://velog.velcdn.com/images/noh_level0/post/a94ae404-20f3-4fd3-b88e-350c7f870d91/image.png" width="90%">
</p>
이는 SpringBootApplication이 실행이 되면서 내장된 톰캣 웹 서버를 띄움과 동시에 Spring Boot가 같이 올라가게 되는 화면이다.

<p><br/><br/></p>
<blockquote>
<p>본 포스팅은 inflearn 김영한 강사님의 &quot;스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술&quot; 강의에 기반하여 작성되었습니다. 
<a href="https://inf.run/hivx6">강의 링크</a><br/>
썸네일 생성: <a href="https://velog.io/@oneook/%EC%8D%B8%EB%84%A4%EC%9D%BC-%EB%A9%94%EC%9D%B4%EC%BB%A4Thumbnail-Maker-Toy-Project">Thumbnail Maker</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>