<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>SihoonCho's.log</title>
        <link>https://velog.io/</link>
        <description>개발을 즐길 줄 아는 백엔드 개발자</description>
        <lastBuildDate>Tue, 07 Apr 2026 16:32:05 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>SihoonCho's.log</title>
            <url>https://velog.velcdn.com/images/sihoon_cho/profile/10f46789-b9b6-4b1d-8cb2-9c87c8f160a3/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. SihoonCho's.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sihoon_cho" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[TypeScript 프로젝트 구조 & 파일 네이밍 컨벤션 (실무 기준 정리)]]></title>
            <link>https://velog.io/@sihoon_cho/TypeScript-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0-%ED%8C%8C%EC%9D%BC-%EB%84%A4%EC%9D%B4%EB%B0%8D-%EC%BB%A8%EB%B2%A4%EC%85%98-%EC%8B%A4%EB%AC%B4-%EA%B8%B0%EC%A4%80-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@sihoon_cho/TypeScript-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0-%ED%8C%8C%EC%9D%BC-%EB%84%A4%EC%9D%B4%EB%B0%8D-%EC%BB%A8%EB%B2%A4%EC%85%98-%EC%8B%A4%EB%AC%B4-%EA%B8%B0%EC%A4%80-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 07 Apr 2026 16:32:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>TypeScript는 Java처럼 “정해진 구조”를 강제하지 않는다.
하지만 실제 프로젝트에서는 <strong>일관된 구조와 네이밍 규칙</strong>이 필수다.
이 글에서는 <strong>공식 개념 + 실무 관례</strong>를 기반으로 정리한다.</p>
</blockquote>
<hr>
<h1 id="1-📌-typescript-구조의-핵심-모듈-시스템">1. 📌 TypeScript 구조의 핵심: 모듈 시스템</h1>
<h2 id="기본-개념">기본 개념</h2>
<pre><code class="language-text">파일 = 모듈</code></pre>
<p>📎 공식 문서
<a href="https://www.typescriptlang.org/docs/handbook/2/modules.html">https://www.typescriptlang.org/docs/handbook/2/modules.html</a></p>
<p>핵심:</p>
<blockquote>
<p>import/export가 존재하면 해당 파일은 모듈로 동작한다</p>
</blockquote>
<hr>
<h2 id="java-vs-typescript">Java vs TypeScript</h2>
<table>
<thead>
<tr>
<th>Java</th>
<th>TypeScript</th>
</tr>
</thead>
<tbody><tr>
<td>클래스 중심</td>
<td>파일(모듈) 중심</td>
</tr>
<tr>
<td>패키지 구조</td>
<td>자유 구조</td>
</tr>
<tr>
<td>상속 기반</td>
<td>조합 기반</td>
</tr>
</tbody></table>
<hr>
<p>👉 결론:</p>
<pre><code class="language-text">TypeScript는 “파일의 역할” 중심으로 구조를 설계해야 한다</code></pre>
<hr>
<h1 id="2-📦-실무에서-가장-많이-쓰는-디렉토리-구조">2. 📦 실무에서 가장 많이 쓰는 디렉토리 구조</h1>
<h2 id="기본-구조">기본 구조</h2>
<pre><code class="language-text">src/
  main.ts
  config/
  modules/
    user/
      user.controller.ts
      user.service.ts
      user.repository.ts
      user.entity.ts
  shared/
    utils/
    errors/</code></pre>
<hr>
<h2 id="구조-특징">구조 특징</h2>
<ul>
<li>feature-based 구조</li>
<li>도메인 중심 구성</li>
<li>역할별 파일 분리</li>
</ul>
<hr>
<h1 id="3-🗂️-파일-네이밍-컨벤션-domainrole">3. 🗂️ 파일 네이밍 컨벤션 (domain.role)</h1>
<h2 id="규칙">규칙</h2>
<pre><code class="language-text">&lt;domain&gt;.&lt;role&gt;.ts</code></pre>
<hr>
<h2 id="예시">예시</h2>
<pre><code class="language-text">user.controller.ts
user.service.ts
user.repository.ts
user.entity.ts</code></pre>
<hr>
<h2 id="장점">장점</h2>
<h3 id="✔-역할이-명확함">✔ 역할이 명확함</h3>
<pre><code class="language-text">user.repository.ts → DB 접근
user.service.ts → 비즈니스 로직</code></pre>
<hr>
<h3 id="✔-정렬이-직관적">✔ 정렬이 직관적</h3>
<pre><code class="language-text">user.controller.ts
user.repository.ts
user.service.ts</code></pre>
<hr>
<h3 id="✔-파일명-충돌-방지">✔ 파일명 충돌 방지</h3>
<pre><code class="language-text">user.ts ❌</code></pre>
<hr>
<h2 id="용어">용어</h2>
<pre><code class="language-text">dot-based naming
role-based naming</code></pre>
<hr>
<h1 id="4-🔤-코드-레벨-네이밍-규칙">4. 🔤 코드 레벨 네이밍 규칙</h1>
<h2 id="클래스">클래스</h2>
<pre><code class="language-ts">export class UserRepository {}</code></pre>
<p>👉 PascalCase</p>
<hr>
<h2 id="함수--변수">함수 / 변수</h2>
<pre><code class="language-ts">const getUser = () =&gt; {}
const userName = &quot;john&quot;;</code></pre>
<p>👉 camelCase</p>
<hr>
<h2 id="상수">상수</h2>
<pre><code class="language-ts">const MAX_RETRY_COUNT = 3;</code></pre>
<p>👉 UPPER_SNAKE_CASE</p>
<hr>
<h2 id="타입--인터페이스">타입 / 인터페이스</h2>
<pre><code class="language-ts">export interface User {}
type UserStatus = &quot;active&quot; | &quot;inactive&quot;;</code></pre>
<p>👉 PascalCase</p>
<hr>
<h2 id="파일-vs-클래스">파일 vs 클래스</h2>
<table>
<thead>
<tr>
<th>대상</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>파일</td>
<td>user.repository.ts</td>
</tr>
<tr>
<td>클래스</td>
<td>UserRepository</td>
</tr>
</tbody></table>
<hr>
<h1 id="🚀-정리">🚀 정리</h1>
<pre><code class="language-text">1. 파일 = 모듈 (핵심 개념)
2. 구조는 자유지만 feature 기반이 일반적
3. 파일명은 domain.role 패턴 사용
4. 클래스 PascalCase, 나머지는 camelCase</code></pre>
<hr>
<h1 id="📎-참고-자료">📎 참고 자료</h1>
<ul>
<li><p>TypeScript Modules
<a href="https://www.typescriptlang.org/docs/handbook/2/modules.html">https://www.typescriptlang.org/docs/handbook/2/modules.html</a></p>
</li>
<li><p>Angular Style Guide
<a href="https://angular.dev/style-guide">https://angular.dev/style-guide</a></p>
</li>
<li><p>Node.js TypeScript Guide
<a href="https://nodejs.org/learn/typescript/publishing-a-ts-package">https://nodejs.org/learn/typescript/publishing-a-ts-package</a></p>
</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 JWT ① - 기본 설정]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-JWT-%EA%B8%B0%EB%B3%B8-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-JWT-%EA%B8%B0%EB%B3%B8-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Wed, 27 Nov 2024 13:42:20 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 생략/과장된 설명이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>


<h1 id="📌-jwt-json-web-token">📌 JWT (JSON Web Token)</h1>
<hr>
<blockquote>
</blockquote>
<h3 id="jwt-json-web-token">JWT (JSON Web Token)</h3>
<p><strong>JWT(JSON Web Token)는 개방형 웹 표준 RFC 7519로 지정된
JSON 객체 형태의 Web Token으로 Token 자체에 정보를 저장하는 인증 방식입니다.</strong></p>
<blockquote>
</blockquote>
<p><strong>OAuth, SAML(Security Assertion Markup Language), OpenID Connect 등과 함께
대표적으로 사용되는 인증 방식으로 다른 인증 방식에 비해 가볍고 간편하다는 장점이 있습니다.</strong></p>
<blockquote>
</blockquote>
<p><a href="https://jwt.io/introduction"><strong>- [출처] JWT 공식 홈페이지 -</strong></a></p>
<blockquote>
<p><em><strong>인증 정보를 담은 <code>Token</code>을 암호화하여 전송</strong></em></p>
</blockquote>
<br>

<h2 id="📖-jwt-구조">📖 JWT 구조</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/eafab2d2-fdb0-466d-88d3-9b52b89c6736/image.png" alt=""></p>
<blockquote>
</blockquote>
<h3 id="jwt-구조">JWT 구조</h3>
<ul>
<li><strong><code>Header</code>, <code>Payload</code>, <code>Signature</code>로 구성</strong><ul>
<li><strong><code>Header</code></strong>: 토큰 타입(typ)과 해싱 알고리즘(alg) 정보를 포함</li>
<li><strong><code>Payload</code></strong>: 사용자 인증 정보와 클레임 데이터를 JSON 형식으로 포함</li>
<li><strong><code>Signature</code></strong>: Header와 Payload를 특정 비밀키로 서명하여 변조 방지</li>
</ul>
</li>
</ul>
<blockquote>
<p><em><strong>→ 인증 정보를 담은 <code>Token</code>을 암호화하여 전송</strong></em></p>
</blockquote>
<br>

<h2 id="📖-jwt-장단점">📖 JWT 장단점</h2>
<hr>
<blockquote>
</blockquote>
<h3 id="장점">장점</h3>
<ul>
<li><strong>빠른 인증 처리</strong>
토큰 자체에 정보를 포함하므로, 데이터베이스 조회 없이 인증 가능</li>
<li><strong>토큰 기반 인증 방식</strong>
중앙 인증서버 방식, 수평적 시스템 확장성 향상
서버 상태를 유지하지 않아도 되므로 확장성 향상</li>
<li><strong>다양한 플랫폼 지원</strong>
표준 규격(JSON)을 사용하여 웹, 모바일, 클라이언트-서버 간 호환성 우수
Base64 URL Safe Encoding을 사용하기 때문에 URL, Cookie, Header 모두 사용 가능</li>
</ul>
<blockquote>
</blockquote>
<h3 id="단점">단점</h3>
<ul>
<li><strong>토큰 크기</strong>
<code>Payload</code>의 정보가 많아지면, 네트워크 사용량 부담 증가</li>
<li><strong>보안 관리 필요</strong>
비밀키가 유출되면 모든 토큰이 위조될 수 있는 위험성 존재</li>
<li><strong>토큰 취소 어려움</strong>
토큰은 클라이언트에 저장되므로, 서버에서 토큰을 조작할 수 없음
토큰은 클라이언트에 저장되므로, 발급 후 유효기간 만료 전에 취소하기 어려움</li>
</ul>
<br>

<h1 id="📌-jwt-기본설정">📌 JWT 기본설정</h1>
<hr>
<blockquote>
</blockquote>
<h3 id="jwt-기본설정">JWT 기본설정</h3>
<p><strong>JWT(JSON Web Token)를 이용한 인증 구현을 위해
<code>build.gradle</code>, <code>application.properties</code>를 설정한다.</strong></p>
<br>

<h2 id="📖-buildgradle">📖 build.gradle</h2>
<hr>
<pre><code class="language-java">dependencies {
    // ...
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;    // 추가
    implementation &#39;io.jsonwebtoken:jjwt-api:0.11.5&#39;      // 추가
    implementation &#39;io.jsonwebtoken:jjwt-impl:0.11.5&#39;        // 추가
    implementation &#39;io.jsonwebtoken:jjwt-jackson:0.11.5&#39;  // 추가
    testImplementation &#39;org.springframework.security:spring-security-test&#39;    // 추가
    // ...
}</code></pre>
<blockquote>
</blockquote>
<h3 id="jjwt-java-jwt">JJWT (Java JWT)</h3>
<ul>
<li><code>jjwt</code> 라이브러리는 버전 <code>0.10.0</code>부터 3개로 분리됨</li>
<li>작성일 기준 <a href="https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api"><strong>Maven Repository</strong></a>에서 가장 많이 사용된 최신 버전 <code>0.11.5</code><ul>
<li><del>과거 <code>implementation &#39;io.jsonwebtoken:jjwt:0.9.1&#39;</code></del></li>
<li><strong>현재 <code>implementation &#39;io.jsonwebtoken:jjwt-api:0.11.5&#39;</code></strong></li>
<li><strong>현재 <code>implementation &#39;io.jsonwebtoken:jjwt-impl:0.11.5&#39;</code></strong></li>
<li><strong>현재 <code>implementation &#39;io.jsonwebtoken:jjwt-jackson:0.11.5&#39;</code></strong></li>
</ul>
</li>
</ul>
<br>

<h2 id="📖-applicationproperties">📖 application.properties</h2>
<hr>
<pre><code class="language-python"># JWT Settings
jwt.secret=your_secret_key
jwt.access.expiration=3600000       # Access Token Expiration: 1hour (ms)
jwt.refresh.expiration=86400000     # Refresh Token Expiration: 24hour (ms)</code></pre>
<blockquote>
</blockquote>
<h3 id="applicationproperties">application.properties</h3>
<ul>
<li><code>jwt.secret</code>: 토큰 서명에 사용되는 비밀키 정의
토큰의 유효성을 확인하는 데 필요</li>
<li><code>jwt.access.expiration</code>: 액세스 토큰의 만료 시간(ms 단위)
일반적으로 짧은 주기로 설정하여 보안 강화</li>
<li><code>jwt.refresh.expiration</code>: 리프레시 토큰의 만료 시간(ms 단위)
액세스 토큰 갱신을 위해 더 긴 주기로 설정</li>
</ul>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 생략/과장된 설명이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 게시판 만들기 Q&A ⑥ - JPQL, 네이티브 쿼리]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-QA-JPQL-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-%EC%BF%BC%EB%A6%AC</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-QA-JPQL-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-%EC%BF%BC%EB%A6%AC</guid>
            <pubDate>Mon, 25 Nov 2024 04:54:06 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>

<br>

<h1 id="📌-jpql">📌 JPQL</h1>
<hr>
<blockquote>
</blockquote>
<p><strong><code>JPQL(Java Persistence Query Language)</code>은 <code>JPA</code>에서 객체(Entity)를 기반으로 데이터를 조회하거나 조작하기 위해 사용하는 <code>쿼리 언어</code>입니다.</strong></p>
<blockquote>
</blockquote>
<p><strong><code>SQL</code>과 유사하지만, 테이블 대신 <code>엔티티</code>와 <code>속성</code>을 대상으로 작업합니다.</strong></p>
<blockquote>
</blockquote>
<h3 id="raw-jpa-vs-spring-data-jpa">Raw JPA vs Spring Data JPA</h3>
<hr>
<p><code>Raw JPA</code>에서는 <code>EntityManager</code>, <code>Spring Data JPA</code>에서는 <code>@Query</code>와 <code>@Param</code>을 활용해 <code>JPQL</code>을 작성하고 실행할 수 있습니다.</p>
<ul>
<li><code>Raw JPA</code>의 <code>EntityManager</code>는 <code>JPQL</code>을 직접 작성하여 데이터베이스와 상호작용합</li>
<li><code>Spring Data JPA</code>에서는 <code>@Query</code>를 사용해 명시적으로 <code>JPQL</code>을 작성하거나,
메서드 이름 기반 쿼리 메서드를 통해 간편하게 <code>JPQL</code> 생성</li>
</ul>
<br>

<h2 id="📖-jpql-개념">📖 JPQL 개념</h2>
<hr>
<blockquote>
</blockquote>
<h3 id="특징">특징</h3>
<ul>
<li>데이터베이스 독립적</li>
<li>테이블이 아닌 클래스와 속성을 기준으로 작성</li>
</ul>
<blockquote>
</blockquote>
<h3 id="작동-원리">작동 원리</h3>
<ul>
<li><strong>JPQL 생성</strong>: <code>JPA</code> 메서드 호출 시 필요에 따라 내부적으로 <code>JPQL</code> 생성</li>
<li><strong>자동 변환</strong>: <code>JPA</code>가 <code>JPQL</code>을 <code>Native SQL</code>로 변환하여 데이터베이스에서 실행</li>
<li><strong>객체 지향 쿼리</strong>: 데이터베이스 테이블이 아닌 <code>JPA</code>의 <code>엔티티</code>를 대상으로 작성</li>
</ul>
<blockquote>
</blockquote>
<h3 id="jpql-vs-native-sql">JPQL vs Native SQL</h3>
<ul>
<li><strong><code>JPQL</code>: <code>&quot;SELECT e FROM Employee e WHERE e.name = :name&quot;</code></strong></li>
<li><strong><code>Native SQL</code>: <code>&quot;SELECT * FROM employee WHERE name = &#39;John&#39;&quot;</code></strong></li>
</ul>
<br>

<h2 id="📖-jpql-예제코드">📖 JPQL 예제코드</h2>
<hr>
<h3 id="✅-패키지-구조">✅ 패키지 구조</h3>
<pre><code>├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com.companyname.projectname/
│   │   │       ├── post/
│   │   │       │   ├── entity
│   │   │       │   │   └── Post.java
│   │   │       │   ├── repository
│   │   │       │   │   └── PostRepository.java
│   │   │       │   ├── service
│   │   │       │   │   └── PostService.java
│   │   │       │   └── controller
│   │   │       │       └── PostController.java
│   │   │       └── ProjectNameApplication.java
│   │   └── resources
│   └── test</code></pre><h3 id="✅-postrepository">✅ <code>PostRepository</code></h3>
<pre><code class="language-java">package com.companyname.projectname.post.repository;

import com.companyname.projectname.post.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface PostRepository extends JpaRepository&lt;Post, Long&gt; {
    // Spring Data JPA JPQL (Java Persistence Query Language) 방식
    @Query(&quot;SELECT p FROM Post p WHERE p.title = :title&quot;)
    List&lt;Post&gt; findByTitle(@Param(&quot;title&quot;) String title);
}</code></pre>
<h3 id="✅-postservice">✅ <code>PostService</code></h3>
<pre><code class="language-java">package com.companyname.projectname.post.service;

import com.companyname.projectname.post.entity.Post;
import com.companyname.projectname.post.repository.PostRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostService {

    private final EntityManager entityManager;
    private final PostRepository postRepository;

    // Spring Data JPA JPQL 방식
    public List&lt;Post&gt; getPostsByTitle(String title) {
        return postRepository.findByTitle(title);
    }

    // EntityManager JPA JPQL 방식 1
    public List&lt;Post&gt; getPostsByTitleJPQL(String title) {
        TypedQuery&lt;Post&gt; query = entityManager.createQuery(
            &quot;SELECT p FROM Post p WHERE p.title = :title&quot;, Post.class);
        query.setParameter(&quot;title&quot;, title);
        return query.getResultList();
    }

    // EntityManager JPA JPQL 방식 2
    public List&lt;Post&gt; getPostsByTitleJPQL(String title) {
        return entityManager.createQuery(
            &quot;SELECT p FROM Post p WHERE p.title = :title&quot;, Post.class)
            .setParameter(&quot;title&quot;, title)
            .getResultList();
    }
}</code></pre>
<h3 id="✅-다양한-응용-예시">✅ <code>다양한 응용 예시</code></h3>
<pre><code class="language-java">package com.companyname.projectname.post.repository;

import com.companyname.projectname.post.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface PostRepository extends JpaRepository&lt;Post, Long&gt; {
    // Spring Data JPA JPQL (Java Persistence Query Language) 기반 메서드

    // 기본 메서드
    @Query(&quot;SELECT p FROM Post p WHERE p.title = :title&quot;)
    List&lt;Post&gt; findByTitle(@Param(&quot;title&quot;) String title);
    @Query(&quot;SELECT p FROM Post p WHERE p.content = :content&quot;)
    List&lt;Post&gt; findByContent(@Param(&quot;content&quot;) String content);
    @Query(&quot;SELECT p FROM Post p WHERE p.title LIKE CONCAT(&#39;%&#39;, :title, &#39;%&#39;)&quot;)
    List&lt;Post&gt; findByTitleContaining(@Param(&quot;title&quot;) String title);
    @Query(&quot;SELECT p FROM Post p WHERE p.content LIKE CONCAT(&#39;%&#39;, :content, &#39;%&#39;)&quot;)
    List&lt;Post&gt; findByContentContaining(@Param(&quot;content&quot;) String content);
    @Query(&quot;SELECT p FROM Post p WHERE LOWER(p.title) LIKE LOWER(CONCAT(&#39;%&#39;, :title, &#39;%&#39;))&quot;)
    List&lt;Post&gt; findByTitleContainingIgnoreCase(@Param(&quot;title&quot;) String title);
    @Query(&quot;SELECT p FROM Post p WHERE LOWER(p.content) LIKE LOWER(CONCAT(&#39;%&#39;, :content, &#39;%&#39;))&quot;)
    List&lt;Post&gt; findByContentContainingIgnoreCase(@Param(&quot;content&quot;) String content);

    // 복합 메서드
    @Query(&quot;SELECT p FROM Post p WHERE p.title = :title AND p.content = :content&quot;)
    List&lt;Post&gt; findByTitleAndContent(@Param(&quot;title&quot;) String title, @Param(&quot;content&quot;) String content);
    @Query(&quot;SELECT p FROM Post p WHERE p.title LIKE CONCAT(&#39;%&#39;, :title, &#39;%&#39;) OR p.content LIKE CONCAT(&#39;%&#39;, :content, &#39;%&#39;)&quot;)
    List&lt;Post&gt; findByTitleContainingOrContentContaining(@Param(&quot;title&quot;) String title, @Param(&quot;content&quot;) String content);
    @Query(&quot;SELECT p FROM Post p WHERE p.title LIKE CONCAT(&#39;%&#39;, :title, &#39;%&#39;) AND p.content LIKE CONCAT(&#39;%&#39;, :content, &#39;%&#39;)&quot;)
    List&lt;Post&gt; findByTitleContainingAndContentContaining(@Param(&quot;title&quot;) String title, @Param(&quot;content&quot;) String content);
    @Query(&quot;SELECT p FROM Post p WHERE LOWER(p.title) LIKE LOWER(CONCAT(&#39;%&#39;, :title, &#39;%&#39;)) OR LOWER(p.content) LIKE LOWER(CONCAT(&#39;%&#39;, :content, &#39;%&#39;))&quot;)
    List&lt;Post&gt; findByTitleContainingIgnoreCaseOrContentContainingIgnoreCase(@Param(&quot;title&quot;) String title, @Param(&quot;content&quot;) String content);
    @Query(&quot;SELECT p FROM Post p WHERE LOWER(p.title) LIKE LOWER(CONCAT(&#39;%&#39;, :title, &#39;%&#39;)) AND LOWER(p.content) LIKE LOWER(CONCAT(&#39;%&#39;, :content, &#39;%&#39;))&quot;)
    List&lt;Post&gt; findByTitleContainingIgnoreCaseAndContentContainingIgnoreCase(@Param(&quot;title&quot;) String title, @Param(&quot;content&quot;) String content);

    // 날짜 기반 메서드 (생성 일자 기준)
    @Query(&quot;SELECT p FROM Post p WHERE p.createdDate &gt; :date&quot;)
    List&lt;Post&gt; findByCreatedDateAfter(@Param(&quot;date&quot;) LocalDateTime date);
    @Query(&quot;SELECT p FROM Post p WHERE p.createdDate &lt; :date&quot;)
    List&lt;Post&gt; findByCreatedDateBefore(@Param(&quot;date&quot;) LocalDateTime date);
    @Query(&quot;SELECT p FROM Post p WHERE p.createdDate BETWEEN :sDate AND :eDate&quot;)
    List&lt;Post&gt; findByCreatedDateBetween(@Param(&quot;sDate&quot;) LocalDateTime sDate, @Param(&quot;eDate&quot;) LocalDateTime eDate);

    // 날짜 기반 메서드 (수정 일자 기준)
    @Query(&quot;SELECT p FROM Post p WHERE p.updatedDate &gt; :date&quot;)
    List&lt;Post&gt; findByUpdatedDateAfter(@Param(&quot;date&quot;) LocalDateTime date);
    @Query(&quot;SELECT p FROM Post p WHERE p.updatedDate &lt; :date&quot;)
    List&lt;Post&gt; findByUpdatedDateBefore(@Param(&quot;date&quot;) LocalDateTime date);
    @Query(&quot;SELECT p FROM Post p WHERE p.updatedDate BETWEEN :sDate AND :eDate&quot;)
    List&lt;Post&gt; findByUpdatedDateBetween(@Param(&quot;sDate&quot;) LocalDateTime sDate, @Param(&quot;eDate&quot;) LocalDateTime eDate);
}</code></pre>
<br>

<h1 id="📌-native-query">📌 Native Query</h1>
<hr>
<blockquote>
</blockquote>
<p><strong><code>Native Query</code>는 <code>SQL</code>을 직접 작성하여 실행하는 방법으로, 복잡한 쿼리나 데이터베이스 특정 기능을 사용할 때 유용합니다. <code>JPA</code>는 네이티브 쿼리를 실행해도 결과를 엔티티로 매핑합니다.</strong></p>
<blockquote>
</blockquote>
<p><strong><code>Spring Data JPA</code>에서 <code>Native Query</code>를 사용할 수도 있습니다.
<code>Spring Data JPA</code>는 작성한 <code>Native SQL</code>을 그대로 데이터베이스에서 실행합니다.</strong></p>
<blockquote>
</blockquote>
<p><em><strong>복잡한 쿼리를 사용할 때 편리하지만, 데이터베이스 의존성이 높아질 수 있습니다.</strong></em></p>
<br>

<h2 id="📖-native-query-개념">📖 Native Query 개념</h2>
<hr>
<blockquote>
</blockquote>
<h3 id="특징-1">특징</h3>
<ul>
<li>리스트로 반환되는 결과나 <code>Pageable</code>의 정렬이 자동으로 적용되지 않습니다.<ul>
<li><code>Native Query</code>에 <code>ORDER BY</code>를 명시적으로 추가</li>
<li><code>Service Layer</code>에서 데이터를 수동으로 정렬 (Java Stream 등 활용)</li>
<li>실무에서는 데이터베이스에서 정렬을 처리하는 것이 더 성능이 좋고 효율적</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<h3 id="작동-원리-1">작동 원리</h3>
<ul>
<li><strong>직접 SQL 작성</strong>: <code>JPQL</code> 대신 <code>SQL</code>을 직접 작성해 데이터베이스에 명령 전달</li>
<li><strong>데이터베이스 종속적</strong>: 특정 DB의 문법을 사용해야 하므로 이식성이 떨어질 수 있음</li>
<li><strong>JPA의 매핑 기능 활용</strong>: 결과를 엔티티에 매핑해 객체 지향적 접근 가능</li>
</ul>
<blockquote>
</blockquote>
<h3 id="사용-목적">사용 목적</h3>
<ul>
<li><strong>복잡한 <code>SQL</code>을 처리해야 하는 경우</strong></li>
<li><strong>데이터베이스 고유 기능이나 최적화가 필요한 경우</strong></li>
</ul>
<br>

<h2 id="📖-native-query-예제코드">📖 Native Query 예제코드</h2>
<hr>
<h3 id="✅-패키지-구조-1">✅ 패키지 구조</h3>
<pre><code>├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com.companyname.projectname/
│   │   │       ├── post/
│   │   │       │   ├── entity
│   │   │       │   │   └── Post.java
│   │   │       │   ├── repository
│   │   │       │   │   └── PostRepository.java
│   │   │       │   ├── service
│   │   │       │   │   └── PostService.java
│   │   │       │   └── controller
│   │   │       │       └── PostController.java
│   │   │       └── ProjectNameApplication.java
│   │   └── resources
│   └── test</code></pre><h3 id="✅-postrepository-1">✅ PostRepository</h3>
<pre><code class="language-java">package com.companyname.projectname.post.repository;

import com.companyname.projectname.post.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface PostRepository extends JpaRepository&lt;Post, Long&gt; {
    // Spring Data JPA Native Query 방식
    @Query(value = &quot;SELECT * FROM post WHERE title = :title ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByTitle(@Param(&quot;title&quot;) String title);
}</code></pre>
<h3 id="✅-postservice-1">✅ <code>PostService</code></h3>
<pre><code class="language-java">package com.companyname.projectname.post.service;

import com.companyname.projectname.post.entity.Post;
import com.companyname.projectname.post.repository.PostRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostService {

    private final EntityManager entityManager;
    private final PostRepository postRepository;

    // Spring Data JPA Native Query 방식
    public List&lt;Post&gt; getPostsByTitle(String title) {
        return postRepository.findByTitle(title);
    }

    // EntityManager JPA Native Query 방식 1
    public List&lt;Post&gt; getPostsByTitleJPQL(String title) {
        TypedQuery&lt;Post&gt; query = entityManager.createNativeQuery(
            &quot;SELECT * FROM post WHERE title = :title&quot;, Post.class);
        query.setParameter(&quot;title&quot;, title);
        return query.getResultList();
    }

    // EntityManager JPA Native Query 방식 2
    public List&lt;Post&gt; getPostsByTitleJPQL(String title) {
        return entityManager.createNativeQuery(
            &quot;SELECT * FROM post WHERE title = :title&quot;, Post.class)
            .setParameter(&quot;title&quot;, title)
            .getResultList();
    }
}</code></pre>
<h3 id="✅-다양한-응용-예시-1">✅ <code>다양한 응용 예시</code></h3>
<pre><code class="language-java">package com.companyname.projectname.post.repository;

import com.companyname.projectname.post.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface PostRepository extends JpaRepository&lt;Post, Long&gt; {
    // Spring Data JPA Native Query 기반 메서드

    // 기본 메서드
    @Query(value = &quot;SELECT * FROM post WHERE title = :title ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByTitle(@Param(&quot;title&quot;) String title);
    @Query(value = &quot;SELECT * FROM post WHERE content = :content ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByContent(@Param(&quot;content&quot;) String content);
    @Query(value = &quot;SELECT * FROM post WHERE title LIKE CONCAT(&#39;%&#39;, :title, &#39;%&#39;) ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByTitleContaining(@Param(&quot;title&quot;) String title);
    @Query(value = &quot;SELECT * FROM post WHERE content LIKE CONCAT(&#39;%&#39;, :content, &#39;%&#39;) ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByContentContaining(@Param(&quot;content&quot;) String content);
    @Query(value = &quot;SELECT * FROM post WHERE LOWER(title) LIKE LOWER(CONCAT(&#39;%&#39;, :title, &#39;%&#39;)) ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByTitleContainingIgnoreCase(@Param(&quot;title&quot;) String title);
    @Query(value = &quot;SELECT * FROM post WHERE LOWER(content) LIKE LOWER(CONCAT(&#39;%&#39;, :content, &#39;%&#39;)) ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByContentContainingIgnoreCase(@Param(&quot;content&quot;) String content);

    // 복합 메서드
    @Query(value = &quot;SELECT * FROM post WHERE title = :title AND content = :content ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByTitleAndContent(@Param(&quot;title&quot;) String title, @Param(&quot;content&quot;) String content);
    @Query(value = &quot;SELECT * FROM post WHERE title LIKE CONCAT(&#39;%&#39;, :title, &#39;%&#39;) OR content LIKE CONCAT(&#39;%&#39;, :content, &#39;%&#39;) ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByTitleContainingOrContentContaining(@Param(&quot;title&quot;) String title, @Param(&quot;content&quot;) String content);
    @Query(value = &quot;SELECT * FROM post WHERE title LIKE CONCAT(&#39;%&#39;, :title, &#39;%&#39;) AND content LIKE CONCAT(&#39;%&#39;, :content, &#39;%&#39;) ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByTitleContainingAndContentContaining(@Param(&quot;title&quot;) String title, @Param(&quot;content&quot;) String content);
    @Query(value = &quot;SELECT * FROM post WHERE LOWER(title) LIKE LOWER(CONCAT(&#39;%&#39;, :title, &#39;%&#39;)) OR LOWER(content) LIKE LOWER(CONCAT(&#39;%&#39;, :content, &#39;%&#39;)) ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByTitleContainingIgnoreCaseOrContentContainingIgnoreCase(@Param(&quot;title&quot;) String title, @Param(&quot;content&quot;) String content);
    @Query(value = &quot;SELECT * FROM post WHERE LOWER(title) LIKE LOWER(CONCAT(&#39;%&#39;, :title, &#39;%&#39;)) AND LOWER(content) LIKE LOWER(CONCAT(&#39;%&#39;, :content, &#39;%&#39;)) ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByTitleContainingIgnoreCaseAndContentContainingIgnoreCase(@Param(&quot;title&quot;) String title, @Param(&quot;content&quot;) String content);

    // 날짜 기반 메서드 (생성 일자 기준)
    @Query(value = &quot;SELECT * FROM post WHERE created_date &gt; :date ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByCreatedDateAfter(@Param(&quot;date&quot;) LocalDateTime date);
    @Query(value = &quot;SELECT * FROM post WHERE created_date &lt; :date ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByCreatedDateBefore(@Param(&quot;date&quot;) LocalDateTime date);
    @Query(value = &quot;SELECT * FROM post WHERE created_date BETWEEN :sDate AND :eDate ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByCreatedDateBetween(@Param(&quot;sDate&quot;) LocalDateTime sDate, @Param(&quot;eDate&quot;) LocalDateTime eDate);

    // 날짜 기반 메서드 (수정 일자 기준)
    @Query(value = &quot;SELECT * FROM post WHERE updated_date &gt; :date ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByUpdatedDateAfter(@Param(&quot;date&quot;) LocalDateTime date);
    @Query(value = &quot;SELECT * FROM post WHERE updated_date &lt; :date ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByUpdatedDateBefore(@Param(&quot;date&quot;) LocalDateTime date);
    @Query(value = &quot;SELECT * FROM post WHERE updated_date BETWEEN :sDate AND :eDate ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByUpdatedDateBetween(@Param(&quot;sDate&quot;) LocalDateTime sDate, @Param(&quot;eDate&quot;) LocalDateTime eDate);
}</code></pre>
<br>

<h1 id="📌-jpa-vs-jpql-vs-native-query">📌 JPA vs JPQL vs Native Query</h1>
<hr>
<h2 id="📖-jpa">📖 JPA</h2>
<pre><code class="language-java">@Repository
public interface PostRepository extends JpaRepository&lt;Post, Long&gt; {
    List&lt;Post&gt; findByTitle(String title);
}</code></pre>
<hr>
<h2 id="📖-jpql">📖 JPQL</h2>
<pre><code class="language-java">@Repository
public interface PostRepository extends JpaRepository&lt;Post, Long&gt; {
    @Query(&quot;SELECT p FROM Post p WHERE p.title = :title&quot;)
    List&lt;Post&gt; findByTitle(@Param(&quot;title&quot;) String title);
}</code></pre>
<hr>
<h2 id="📖-native-query">📖 Native Query</h2>
<pre><code class="language-java">@Repository
public interface PostRepository extends JpaRepository&lt;Post, Long&gt; {
    @Query(value = &quot;SELECT * FROM post WHERE title = :title ORDER BY id ASC&quot;, nativeQuery = true)
    List&lt;Post&gt; findByTitle(@Param(&quot;title&quot;) String title);
}</code></pre>
<hr>
<blockquote>
</blockquote>
<h3 id="차이점-정리">차이점 정리</h3>
<ul>
<li><code>JPQL</code> : <code>&quot;SELECT p FROM Post p WHERE p.title = :title&quot;</code></li>
<li><code>Native SQL</code> : <code>&quot;SELECT * FROM post WHERE title = :title&quot;</code></li>
</ul>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 게시판 만들기 Q&A ⑤ - Spring Data JPA]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-QA-Spring-Data-JPA</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-QA-Spring-Data-JPA</guid>
            <pubDate>Sun, 24 Nov 2024 07:37:47 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>

<br>

<h1 id="📌-jpa">📌 JPA</h1>
<hr>
<blockquote>
</blockquote>
<p><strong>JPA(Java Persistence API)는, Java 애플리케이션에서 데이터베이스 작업을 간소화하기 위해 제공되는 ORM(Object-Relational Mapping) 기술 표준입니다. JPA를 활용하면 객체 지향 프로그래밍 방식으로 데이터베이스를 다룰 수 있어 개발 생산성과 코드의 가독성이 크게 향상됩니다.</strong></p>
<br>

<h2 id="📖-jpa-개념">📖 JPA 개념</h2>
<hr>
<blockquote>
</blockquote>
<h3 id="ormobject-relational-mapping">ORM(Object-Relational Mapping)</h3>
<ul>
<li>객체와 데이터베이스 테이블 간의 매핑을 자동으로 처리</li>
<li>SQL을 직접 작성하지 않고도 객체를 사용해 데이터베이스 작업 가능</li>
</ul>
<blockquote>
</blockquote>
<h3 id="jpajava-persistence-api">JPA(Java Persistence API)</h3>
<ul>
<li>JPA는 인터페이스 기반의 표준 사양으로, Hibernate 같은 구현체를 사용해 동작</li>
<li>데이터베이스와의 연결, CRUD 작업, 쿼리 작성 등을 간편하게 처리</li>
</ul>
<br>

<h1 id="📌-spring-data-jpa">📌 Spring Data JPA</h1>
<hr>
<blockquote>
</blockquote>
<p><strong>JPA(Java Persistence API)는, Java 애플리케이션에서 데이터베이스 작업을 간소화하기 위해 제공되는 ORM(Object-Relational Mapping) 기술 표준입니다. JPA를 활용하면 객체 지향 프로그래밍 방식으로 데이터베이스를 다룰 수 있어 개발 생산성과 코드의 가독성이 크게 향상됩니다.</strong></p>
<blockquote>
</blockquote>
<p><strong><code>Spring Framework</code>에서 흔히 사용하는 <code>Spring Data JPA</code>는 <code>Spring Framework</code>와 <code>JPA</code>를 통합하여 데이터베이스 작업을 더 간단하게 처리할 수 있도록 도와주는 라이브러리 모듈입니다.</strong></p>
<br>

<h2 id="📖-spring-data-jpa-개념">📖 Spring Data JPA 개념</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/e4be741f-dc3a-4801-bad0-c16c3cb528cc/image.png" alt=""></p>
<blockquote>
</blockquote>
<p><strong><code>Spring Framework</code>에서 흔히 사용하는 <code>Spring Data JPA</code>는 <code>Spring Framework</code>와 <code>JPA</code>를 통합하여 데이터베이스 작업을 더 간단하게 처리할 수 있도록 도와주는 라이브러리 모듈입니다.</strong></p>
<blockquote>
</blockquote>
<p><strong>인터페이스만 정의하면 기본적인 CRUD 기능이 자동으로 구현되며, <code>@Query</code>를 사용해 복잡한 <code>JPQL</code>이나 <code>Native Query</code>도 지원합니다. <code>JPA</code>의 강력한 기능을 <code>Spring</code> 환경에서 쉽게 활용할 수 있으며, 생산성을 크게 향상시킵니다.</strong></p>
<br>

<h2 id="📖-spring-data-jpa-동작-원리">📖 Spring Data JPA 동작 원리</h2>
<hr>
<blockquote>
</blockquote>
<p><strong><code>Spring Data JPA</code>의 메서드 호출은 상황에 따라 <code>JPQL</code>을 생성합니다.
이후, 다시 <code>Native SQL</code>로 변환되어 데이터베이스에서 실행됩니다.</strong></p>
<blockquote>
</blockquote>
<h3 id="spring-data-jpa-동작-과정">Spring Data JPA 동작 과정</h3>
<ol>
<li>메서드 호출</li>
<li><code>JPQL</code> 생성<ul>
<li><code>JPQL</code> 생성이 필요한 경우<ul>
<li><code>findByName(String name)</code> 같은 쿼리 메서드는 <code>JPA</code>가 내부적으로 <code>JPQL</code> 생성</li>
</ul>
</li>
<li><code>JPQL</code> 생성이 필요하지 않은 경우<ul>
<li><code>find()</code>, <code>save()</code>, <code>delete()</code> 같은 기본 메서드는 바로 <code>Native SQL</code> 생성</li>
<li><code>JPQL</code>을 명시적으로 작성한 경우 <code>JPA</code>가 이를 직접 <code>Native SQL</code>로 변환</li>
</ul>
</li>
</ul>
</li>
<li><code>JPQL</code>을 <code>Native SQL</code>로 변환</li>
<li><code>Native SQL</code> 데이터베이스에 전달 및 실행</li>
<li><code>SQL</code> 실행 결과를 다시 <code>JPA</code> 엔티티 객체로 매핑하여 반환</li>
</ol>
<br>

<h1 id="📌-결론">📌 결론</h1>
<hr>
<blockquote>
</blockquote>
<p><code>JPA</code>는 기본적으로 <code>Native SQL</code>을 실행하기 위한 도구입니다.
<code>JPQL</code>은 <code>Native SQL</code>을 생성하기 위한 중간 단계로 사용됩니다.</p>
<blockquote>
</blockquote>
<p>그러나, <code>JPA</code>의 메서드 호출이 항상 <code>JPQL</code>을 생성하는 것은 아닙니다.
상황에 따라 바로 <code>Native SQL</code>로 변환하거나 생성하기도 합니다.</p>
<blockquote>
</blockquote>
<p><code>Spring Data JPA</code>에서 일반적인 메서드를 호출하는 경우 <code>JPQL</code>이 생성될 수 있습니다.
그러나, <code>JPA</code>의 기본 메서드 <code>find()</code>, <code>save()</code>, <code>delete()</code> 등을 호출하는 경우 <code>JPQL</code> 생성 없이 바로 <code>Native SQL</code>을 생성하거나, 명시적인 <code>JPQL</code>을 작성하는 경우 바로 <code>Native SQL</code>로 변환되어 데이터베이스에서 실행됩니다.</p>
<blockquote>
</blockquote>
<p><strong><code>Spring Data JPA</code>의 전체 과정은 아래와 같이 요약할 수 있습니다</strong></p>
<blockquote>
</blockquote>
<p><em><strong>정리하면, <code>JPA</code>는 메서드를 호출하면 필요에 따라 내부적으로 <code>JPQL</code>로 변환한 뒤,
이를 다시 <code>Native SQL</code>로 변환해서 데이터베이스에서 실행합니다.</strong></em></p>
<pre><code class="language-plaintext">메서드 호출 → (필요 시) JPQL 생성 → Native SQL 변환 → SQL 실행 → 결과를 객체로 매핑</code></pre>
<br>

<h2 id="📖-jpa-장단점">📖 JPA 장단점</h2>
<hr>
<blockquote>
</blockquote>
<h3 id="jpa-장점">JPA 장점</h3>
<ul>
<li>생산성향상: SQL 작성 부담을 줄이고, 객체 중심의 개발 가능</li>
<li>유지보수성: 데이터베이스 변경 시, 객체 중심의 수정으로 유지보수 용이</li>
<li>성능최적화: 지연 로딩, 캐싱, 배치 처리 등 성능 최적화 지원</li>
<li>DB 독립성: 데이터베이스에 종속되지 않는 애플리케이션 개발 가능</li>
</ul>
<blockquote>
</blockquote>
<h3 id="jpa-단점">JPA 단점</h3>
<ul>
<li>학습 곡선: 객체와 데이터베이스 간 매핑 개념 이해 필요</li>
<li>처리 한계: 복잡한 SQL이 필요한 경우 <code>JPQL</code>, <code>Native Query</code> 사용</li>
<li>초기 설정: 초기 설정 방법이 다소 복잡<ul>
<li>ex) persistence.xml, Spring Data JPA 설정 등</li>
</ul>
</li>
</ul>
<br>

<h2 id="📖-jpa를-사용해야-하는가">📖 JPA를 사용해야 하는가?</h2>
<hr>
<blockquote>
</blockquote>
<p><strong><code>JPA</code>는 객체 지향과 데이터베이스의 간극을 메우는 강력한 기술입니다. <code>Java</code> 개발자에게 <code>JPA</code>는 생산성과 객체 지향 설계의 조화를 제공하는 강력한 도구입니다. 특히, <code>Spring Boot</code>, <code>Spring Data JPA</code>를 통해 <code>CRUD 작업</code>, <code>Repository 패턴</code> 쉽게 구현할 수 있습니다.</strong></p>
<blockquote>
</blockquote>
<p><strong>처음에는 조금 복잡하게 느껴질 수 있지만,
프로젝트 규모가 커질수록 그 가치를 실감할 수 있습니다.
<code>Java</code>로 데이터베이스 작업을 한다면, <code>JPA</code>는 선택이 아닌 필수라고 할 수 있습니다.</strong></p>
<blockquote>
</blockquote>
<p><em><strong>반복적인 <code>CRUD SQL 작성</code>과 <code>객체를 SQL에 Mapping</code>하는데 시간을 보내기에는, 개발자의 시간이 너무 아깝습니다. 이미 많은 <code>Java</code> 개발자들이 오랫동안 비슷한 고민을 해왔으며, 이 문제를 해결하기 위해 많은 노력을 기울여왔습니다. 그리고 그 노력의 결정체가 바로 <code>JPA</code>입니다.</strong></em></p>
<blockquote>
</blockquote>
<p><em><strong><code>JPA</code>는 표준 명세만 570 페이지에 달하며, <code>JPA</code>를 구현한 <code>Hibernate</code>는 10년 이상 지속적으로 개발되고 있고, 핵심 모듈의 코드가 이미 10만 줄을 넘어섰습니다. 귀찮은 문제들은 이제 <code>JPA</code>에게 맡기고, 더 좋은 객체 모델링과 더 많은 테스트를 작성하는데 개발자의 시간을 보내세요.</strong></em></p>
<blockquote>
</blockquote>
<p><em><strong>개발자는 <code>SQL Mapper</code>가 아닙니다.</strong></em></p>
<blockquote>
</blockquote>
<p><em><strong>- 출처: 자바 ORM 표준 JPA 프로그래밍 / 저자: 김영한 -</strong></em></p>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 게시판 만들기 Q&A ④ - @Builder 빌더 패턴]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-QA-Builder-%EB%B9%8C%EB%8D%94-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-QA-Builder-%EB%B9%8C%EB%8D%94-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Sat, 23 Nov 2024 13:41:33 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>

<br>

<h1 id="📌-builder-빌더-패턴">📌 @Builder 빌더 패턴</h1>
<hr>
<p><strong><code>Lombok</code>의 <code>@Builder</code> 어노테이션은 객체를 빌더 패턴으로 생성할 수 있게 도와줍니다.</strong>
<strong>클래스와 생성자 중 <code>@Builder</code>를 어디에 붙이는지에 따라 중요한 차이가 있습니다.</strong>
<strong>다음은 두 방식의 차이점입니다.</strong></p>
<br>

<h2 id="📖-1-클래스에-builder를-붙이는-경우">📖 1. 클래스에 @Builder를 붙이는 경우</h2>
<hr>
<pre><code class="language-java">import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class User {
    private String name;
    private int age;
    private String email;
}

// 사용 예시
User user = User.builder()
    .name(&quot;John Doe&quot;)
    .age(30)
    .email(&quot;john.doe@example.com&quot;)
    .build();

// 출력: User(name=John Doe, age=30, email=john.doe@example.com)
System.out.println(user);</code></pre>
<blockquote>
</blockquote>
<ul>
<li>기본적으로 <code>@AllArgsConstructor</code>를 활용하여 빌더가 객체 생성</li>
<li>클래스의 모든 필드(멤버 변수)를 대상으로 빌더 생성</li>
<li>클래스의 필드(멤버 변수)가 많을 때 유용</li>
</ul>
<p><strong>클래스에 @Builder를 사용하는 경우, 모든 필드(멤버 변수)를 포함하는 빌더가 생성됩니다.</strong></p>
<br>

<h2 id="📖-2-생성자에-builder를-붙이는-경우">📖 2. 생성자에 @Builder를 붙이는 경우</h2>
<hr>
<pre><code class="language-java">import lombok.Builder;
import lombok.ToString;

@ToString
public class User {
    private String name;
    private int age;
    private String email;

    @Builder
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

// 사용 예시
User user = User.builder()
    .name(&quot;John Doe&quot;)
    .email(&quot;john.doe@example.com&quot;)
    .build();

// 출력: User(name=John Doe, age=0, email=john.doe@example.com)
System.out.println(user);</code></pre>
<blockquote>
</blockquote>
<ul>
<li>기본적으로 <code>@AllArgsConstructor</code>를 활용하여 빌더가 객체를 생성하므로,
이를 이용해 제한된 필드로 객체 생성</li>
<li>생성자에 포함된 필드(매개변수)만을 대상으로 빌더 생성</li>
<li>클래스의 일부 필드(멤버 변수)만 초기화하려는 경우 적합</li>
</ul>
<p><strong>생성자에 @Builder를 사용하는 경우, 해당 생성자의 매개변수만을 포함하는 빌더가 생성됩니다.</strong>
위 예제에서 <code>age</code>는 생성자의 매개변수에 포함되지 않았으므로 빌더로 초기화되지 않습니다.
<code>age</code>는 기본값인 0으로 설정됩니다.</p>
<br>

<h1 id="📌-차이점-요약">📌 차이점 요약</h1>
<hr>
<blockquote>
<ul>
<li><code>@Builder</code>를 클래스에 사용하는 경우</li>
</ul>
</blockquote>
<ul>
<li>비교적 코드 간결</li>
<li>클래스의 모든 필드 포함</li>
<li>모든 필드의 초기화가 필요한 경우 적합</li>
</ul>
<blockquote>
<ul>
<li><code>@Builder</code>를 생성자에 사용하는 경우</li>
</ul>
</blockquote>
<ul>
<li>생성자 추가 작성이 필요하므로 비교적 코드가 간결하지 않음</li>
<li>생성자의 매개변수에 해당하는 필드만 포함</li>
<li>특정 필드만 초기화가 필요하거나, 여러 생성자를 구분하는 경우 적합</li>
</ul>
<p>추천 사용 방식</p>
<ul>
<li>클래스에 <code>@Builder</code> 사용: 모든 필드의 초기화가 필요한 경우</li>
<li>생성자에 <code>@Builder</code> 사용: 특정 필드만 초기화가 필요하거나, 여러 생성자를 구분하는 경우</li>
</ul>
<p><em><strong>이 두 방식을 적절히 조합하면 더욱 유연한 객체 생성 패턴을 설계할 수 있습니다.</strong></em></p>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 게시판 만들기 Q&A ② - 데이터의 흐름]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%ED%9D%90%EB%A6%84</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%ED%9D%90%EB%A6%84</guid>
            <pubDate>Sat, 23 Nov 2024 05:47:23 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>

<br>

<h1 id="📌-데이터의-흐름">📌 데이터의 흐름</h1>
<hr>
<p><strong>실습에 앞서 데이터의 흐름을 알아야 합니다.</strong>
<strong></p>
<pre><code>├── domain(model)
├── dto
├── repository
├── service
├── controller</code></pre></strong>

<p><strong>아, 벌써부터 머리가 막 어지럽고 인터넷 창을 닫고싶어집니다.
그러니 여러분을 위해 이게 의미하는 바를 간단히 요약해보겠습니다.</strong></p>
<h2 id="📖-데이터의-흐름">📖 데이터의 흐름</h2>
<hr>
<blockquote>
<p><strong>데이터의 흐름은 아래의 순서를 거친다.</strong>
<strong>DB(domain) -&gt; repository -&gt; service -&gt; dto -&gt; controller</strong>
<strong>DB(domain) &lt;- repository &lt;- service &lt;- dto &lt;- controller</strong></p>
</blockquote>
<p><strong>엄밀히 말하면 <code>domain</code>은 <code>DB</code>가 아니라, <code>테이블 정의</code>입니다.
편의상 기억하기 좋고 이해를 돕기 위해 저렇게 분류를 해두었습니다.</strong></p>
<hr>
<p><strong>간단히 정리하면 아래의 핵심만 기억하면 됩니다.</strong></p>
<blockquote>
<p><strong>Service 단을 거칠 때, Entity가 Dto로 변환(복사)된다.</strong></p>
</blockquote>
<p><strong>또 다시 의문이 생깁니다.</strong></p>
<blockquote>
<p><em><strong>&quot;그래서 <code>Entity</code>는 뭐고 <code>DTO</code>는 뭔데?!&quot;</strong></em></p>
</blockquote>
<br>

<h2 id="📖-entity-vs-dto">📖 Entity vs DTO</h2>
<hr>
<blockquote>
<p><strong><code>Entity</code>: 실제 DB에 저장된 데이터 하나 하나를 의미하는 단위</strong>
<strong><code>DTO</code>: 실제 데이터인 <code>Entity</code>를 필요한 속성만 뽑아 복사한 데이터</strong></p>
</blockquote>
<p><strong>그러니까 정리하면 <code>Service 단을 거칠 때 데이터를 복사하여 전달하겠다</code>는 것입니다.</strong></p>
<blockquote>
<p><em><strong>&quot;왜 번거롭게 DTO로 복사를 하지? 그냥 Entity를 바로 가져다 쓰면 안 돼?!&quot;</strong></em></p>
</blockquote>
<p><strong>개발을 좀 공부해보셨다면 &#39;은행원 알고리즘&#39; 문제를 아실겁니다.
&#39;<em>A가 입금하는 동안, B가 출금하면, 최종잔고에 문제가 생긴다.</em>&#39; 라는 것인데,
물론 <code>Entity</code>로 처리하더라도 은행원 알고리즘이 완벽하면 문제는 없습니다.</strong></p>
<hr>
<p><strong>하지만, 이런 비슷한 다른 문제가 발생한다면?
실제로 개발을 진행했을 때 이런 문제가 발생하면 오류가 난 상태로 DB에 저장됩니다.</strong></p>
<p><strong>만약 결제 시스템이나 핀테크 기능이었다면 실제로 금전문제가 발생할 수 있고,
법적 소송까지 이어지는 상당히 민감한 문제가 발생할 수 있기 때문에,
&#39;<code>Entity</code>를 복사하는 한 단계&#39;를 더 거쳐 안전성을 확보하겠다는 것입니다.</strong></p>
<p>*<em>그 외에도 게시판 목록같은 경우 내용에 관련한 데이터는 필요 없기 때문에
불필요한 데이터는 제거하고 성능을 향상시키기 위한 목적도 있습니다.
*</em></p>
<hr>
<p><strong>주저리 주저리 떠들었습니다만, 간단히 요약하면 아래와 같습니다.</strong></p>
<blockquote>
<p><em><strong>&quot;나는, 제목만 필요하고 내용은 필요없으니까, 커스터마이징할거야!&quot;</strong></em>
<em><strong>&quot;그런데, 혹시 오류가 발생할 수도 있으니까, 복사본을 사용하는거야!&quot;</strong></em></p>
</blockquote>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 게시판 만들기 실습 ⑥ - CommonResponse & 예외처리]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-ResponseData</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-ResponseData</guid>
            <pubDate>Fri, 22 Nov 2024 06:36:52 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>

<br>

<h1 id="📌-패키지-구조">📌 패키지 구조</h1>
<hr>
<strong>

<pre><code>├── src
│   ├── main
│   │   ├── java
│   │   │   └── com.companyname.projectname
│   │   │       ├── common                                // 패키지 추가
│   │   │       │   ├── controller                        // 패키지 추가
│   │   │       │   │   └── CommonResponseController.java    // 클래스 추가
│   │   │       │   ├── dto                                // 패키지 추가
│   │   │       │   │   └── CommonResponse.java                // 클래스 추가
│   │   │       │   ├── exception                        // 패키지 추가
│   │   │       │   │   ├── CustomNotFoundException.java        // 클래스 추가
│   │   │       │   │   └── CustomValidateException.java        // 클래스 추가
│   │   │       │   └── handler                            // 패키지 추가
│   │   │       │       ├── CustomExceptionHandler.java        // 클래스 추가
│   │   │       │       └── GlobalExceptionHandler.java        // 클래스 추가
│   │   │       ├── post
│   │   │       │   ├── controller
│   │   │       │   │   ├── PostControllerV1.java    // 클래스 수정
│   │   │       │   │   └── PostControllerV2.java    // 클래스 추가
│   │   │       │   ├── dto
│   │   │       │   │   ├── PostCreateRequestDto.java
│   │   │       │   │   ├── PostUpdateRequestDto.java
│   │   │       │   │   ├── PostDetailResponseDto.java
│   │   │       │   │   └── PostListResponseDto.java
│   │   │       │   ├── model
│   │   │       │   │   └── Post.java
│   │   │       │   ├── repository
│   │   │       │   │   └── PostRepository.java
│   │   │       │   └── service
│   │   │       │       ├── PostReadService.java
│   │   │       │       ├── PostReadServiceImpl.java
│   │   │       │       ├── PostWriteService.java
│   │   │       │       └── PostWriteServiceImpl.java
│   │   │       └── ProjectNameApplication.java
│   │   └── resources
│   └── test</code></pre></strong>

<br>

<h1 id="📌-commonresponse">📌 CommonResponse</h1>
<hr>
<blockquote>
</blockquote>
<p><strong><code>CommonResponse</code>는 API 응답 형식을 표준화한 응답 객체입니다.</strong></p>
<ul>
<li>클라이언트와 서버 간 일관성 제공</li>
<li>가독성과 유지보수성 향상<ul>
<li>성공 응답 및 에러 응답 구분</li>
<li>상태 코드, 메시지, 데이터 포함  <blockquote>
</blockquote>
<em><strong>이를 통해 각 <code>API</code>에서 통일된 구조의 반환 형식을 사용할 수 있습니다.</strong></em></li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<p><strong>이번 글에서는 기존 <code>PostController</code>에 <code>CommonResponse</code>를 적용하여
최적화 및 성능 개선을 해보겠습니다.</strong></p>
<blockquote>
</blockquote>
<h3 id="postcontroller-최적화-및-성능-개선">PostController 최적화 및 성능 개선</h3>
<ul>
<li><code>PostController</code> → <code>PostControllerV1</code> 클래스명 변경 </li>
<li><code>PostControllerV2</code>: 최적화 및 개선된 컨트롤러 추가<ul>
<li><code>URL Mapping</code> 최적화 → <code>RequestMapping</code> 적용</li>
<li><code>CommonResponse</code> 적용</li>
</ul>
</li>
</ul>
<br>

<h2 id="📖-commonresponse">📖 CommonResponse</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.common.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CommonResponse&lt;T&gt; {
    private String message;
    private int status;
    private T data;

    // Success Response
    public static CommonResponse&lt;Void&gt; success() {
        return CommonResponse.&lt;Void&gt;builder()
                .message(&quot;The request was successful&quot;)
                .status(200)
                .build();
    }

    public static &lt;T&gt; CommonResponse&lt;T&gt; success(T data) {
        return CommonResponse.&lt;T&gt;builder()
                .message(&quot;The request was successful&quot;)
                .status(200)
                .data(data)
                .build();
    }

    public static &lt;T&gt; CommonResponse&lt;T&gt; success(String message, T data) {
        return CommonResponse.&lt;T&gt;builder()
                .message(message)
                .status(200)
                .data(data)
                .build();
    }

    public static &lt;T&gt; CommonResponse&lt;T&gt; success(int status, String message, T data) {
        return CommonResponse.&lt;T&gt;builder()
                .message(message)
                .status(status)
                .data(data)
                .build();
    }

    // Error Response
    public static CommonResponse&lt;Void&gt; error() {
        return CommonResponse.&lt;Void&gt;builder()
                .message(&quot;Internal Server Error: An unexpected error occurred.&quot;)
                .status(500)
                .build();
    }

    public static CommonResponse&lt;Void&gt; error(int status) {
        String message = switch (status) {
            case 400 -&gt; &quot;Bad Request: The request is invalid.&quot;;
            case 401 -&gt; &quot;Unauthorized: Authentication is required.&quot;;
            case 403 -&gt; &quot;Forbidden: Access to the resource is denied.&quot;;
            case 404 -&gt; &quot;Not Found: The requested resource could not be found.&quot;;
            case 500 -&gt; &quot;Internal Server Error: An unexpected error occurred.&quot;;
            default -&gt; &quot;Unexpected Error: An unknown error occurred. Please contact support.&quot;;
        };

        return CommonResponse.&lt;Void&gt;builder()
                .message(message)
                .status(status)
                .build();
    }

    public static CommonResponse&lt;Void&gt; error(int status, String message) {
        return CommonResponse.&lt;Void&gt;builder()
                .message(message)
                .status(status)
                .build();
    }

    public static &lt;T&gt; CommonResponse&lt;T&gt; error(int status, String message, T data) {
        return CommonResponse.&lt;T&gt;builder()
                .message(message)
                .status(status)
                .data(data)
                .build();
    }
}</code></pre>
<blockquote>
</blockquote>
<p><strong><code>CommonResponse</code>는 API 응답 형식을 표준화한 응답 객체입니다.</strong></p>
<ul>
<li>성공 응답 및 에러 응답 구분</li>
<li>상태 코드, 메시지, 데이터 포함</li>
</ul>
<br>

<h2 id="📖-postcontrollerv1">📖 PostControllerV1</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.controller;

import com.companyname.projectname.post.dto.PostCreateRequestDto;
import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostListResponseDto;
import com.companyname.projectname.post.dto.PostUpdateRequestDto;
import com.companyname.projectname.post.service.PostReadService;
import com.companyname.projectname.post.service.PostWriteService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class PostControllerV1 {
    private final PostReadService postReadService;
    private final PostWriteService postWriteService;

    @GetMapping(&quot;/api/v1/post/list&quot;)
    public List&lt;PostListResponseDto&gt; getOverviewPosts() {
        return postReadService.findAllLists();
    }

    @GetMapping(&quot;/api/v1/post/list/detail&quot;)
    public List&lt;PostDetailResponseDto&gt; getDetailedPosts() {
        return postReadService.findAllDetails();
    }

    @GetMapping(&quot;/api/v1/post/{id}&quot;)
    public PostDetailResponseDto getPost(@PathVariable Long id) {
        return postReadService.findById(id);
    }

    @PostMapping(&quot;/api/v1/post&quot;)
    public PostDetailResponseDto createPost(@RequestBody PostCreateRequestDto requestDto) {
        return postWriteService.create(requestDto);
    }

    @PutMapping(&quot;/api/v1/post&quot;)
    public PostDetailResponseDto updatePost(@RequestBody PostUpdateRequestDto requestDto) {
        return postWriteService.update(requestDto);
    }

    @DeleteMapping(&quot;/api/v1/post/{id}&quot;)
    public void deletePost(@PathVariable Long id) {
        postWriteService.delete(id);
    }
}</code></pre>
<blockquote>
</blockquote>
<h3 id="postcontrollerv1-최적화-및-성능-개선">PostControllerV1 최적화 및 성능 개선</h3>
<ul>
<li><code>PostController</code> → <code>PostControllerV1</code> 클래스명 변경 </li>
</ul>
<br>

<h2 id="📖-postcontrollerv2">📖 PostControllerV2</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.controller;

import com.companyname.projectname.common.dto.CommonResponse;
import com.companyname.projectname.post.dto.PostCreateRequestDto;
import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostListResponseDto;
import com.companyname.projectname.post.dto.PostUpdateRequestDto;
import com.companyname.projectname.post.service.PostReadService;
import com.companyname.projectname.post.service.PostWriteService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api/v2/post&quot;)
public class PostControllerV2 {
    private final PostReadService postReadService;
    private final PostWriteService postWriteService;

    @GetMapping(&quot;/list&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; getOverviewPosts() {
        List&lt;PostListResponseDto&gt; responseDto = postReadService.findAllLists();
        return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.success(responseDto));
    }

    @GetMapping(&quot;/list/detail&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; getDetailedPosts() {
        List&lt;PostDetailResponseDto&gt; responseDto = postReadService.findAllDetails();
        return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.success(responseDto));
    }

    @GetMapping(&quot;/{id}&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; getPost(@PathVariable Long id) {
        PostDetailResponseDto responseDto = postReadService.findById(id);
        return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.success(responseDto));
    }

    @PostMapping
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; createPost(@RequestBody PostCreateRequestDto requestDto) {
        PostDetailResponseDto responseDto = postWriteService.create(requestDto);
        return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.success(responseDto));
    }

    @PutMapping
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; updatePost(@RequestBody PostUpdateRequestDto requestDto) {
        PostDetailResponseDto responseDto = postWriteService.update(requestDto);
        return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.success(responseDto));
    }

    @DeleteMapping(&quot;/{id}&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; deletePost(@PathVariable Long id) {
        postWriteService.delete(id);
        return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.success());
    }
}</code></pre>
<blockquote>
</blockquote>
<h3 id="postcontrollerv2-최적화-및-성능-개선">PostControllerV2 최적화 및 성능 개선</h3>
<ul>
<li><code>PostControllerV2</code>: 최적화 및 개선된 컨트롤러<ul>
<li><code>URL Mapping</code> 최적화 → <code>RequestMapping</code> 적용</li>
<li><code>CommonResponse</code> 적용</li>
</ul>
</li>
</ul>
<br>

<h2 id="📖-crcontroller-api-테스트">📖 CRController API 테스트</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.common.controller;

import com.companyname.projectname.common.dto.CommonResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping(&quot;/api/v1/common-response&quot;)
public class CommonResponseController {
    private static final int NON_STANDARD_SUCCESS_CODE = 700;
    private static final int NON_STANDARD_FAILURE_CODE = 800;
    private static final String NON_STANDARD_SUCCESS_MSG = &quot;Nonstandard success response message.&quot;;
    private static final String NON_STANDARD_FAILURE_MSG = &quot;Nonstandard failure response message.&quot;;
    private static final Map&lt;String, String&gt; dummyData = Map.of(
            &quot;key1&quot;, &quot;value1&quot;,
            &quot;key2&quot;, &quot;value2&quot;,
            &quot;key3&quot;, &quot;value3&quot;
    );

    // Success REST API
    @GetMapping(&quot;/success&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; success() {
        return ResponseEntity.status(HttpStatus.OK)
                .body(CommonResponse.success());
    }

    @GetMapping(&quot;/success/data&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; successWithData() {
        return ResponseEntity.status(HttpStatus.OK)
                .body(CommonResponse.success(dummyData));
    }

    @GetMapping(&quot;/success/message-data&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; successWithMessageAndData() {
        return ResponseEntity.status(HttpStatus.OK)
                .body(CommonResponse.success(&quot;Custom Message&quot;, dummyData));
    }

    @GetMapping(&quot;/success/custom&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; successWithCustom() {
        return ResponseEntity.status(NON_STANDARD_SUCCESS_CODE)
                .body(CommonResponse.success(NON_STANDARD_SUCCESS_CODE, NON_STANDARD_SUCCESS_MSG, dummyData));
    }

    // Error REST API
    @GetMapping(&quot;/error&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; error() {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(CommonResponse.error());
    }

    @GetMapping(&quot;/error/status/{status}&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; errorWithStatus(@PathVariable Integer status) {
        return ResponseEntity.status(status)
                .body(CommonResponse.error(status));
    }

    @GetMapping(&quot;/error/custom&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; errorWithCustom() {
        return ResponseEntity.status(NON_STANDARD_FAILURE_CODE)
                .body(CommonResponse.error(NON_STANDARD_FAILURE_CODE, NON_STANDARD_FAILURE_MSG));
    }

    @GetMapping(&quot;/error/detail&quot;)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; errorWithDetail() {
        return ResponseEntity.status(NON_STANDARD_FAILURE_CODE)
                .body(CommonResponse.error(NON_STANDARD_FAILURE_CODE, NON_STANDARD_FAILURE_MSG, dummyData));
    }
}</code></pre>
<br>

<h1 id="📌-예외처리">📌 예외처리</h1>
<hr>
<blockquote>
</blockquote>
<h3 id="exception">Exception</h3>
<ul>
<li><code>Exception</code>: 컴파일 시 발생하는 검사 예외(Checked Exception) 클래스<ul>
<li>컴파일 시 예외 처리 요구, 처리하지 않으면 프로그램이 컴파일되지 않음<ul>
<li>Ex) <code>IOException</code>, <code>SQLException</code></li>
</ul>
</li>
</ul>
</li>
<li><code>RuntimeException</code>: 런타임 시 발생하는 비검사 예외(Unchecked Exception) 클래스<ul>
<li>런타임(실행 중) 시 발생하며, 예외를 처리하지 않으면 프로그램 중단</li>
<li>컴파일 시에는 오류 발생하지 않음, 실행 중 예외 발생 전까지는 프로그램 정상 작동<ul>
<li>Ex) <code>NullPointerException</code>, <code>IllegalArgumentException</code></li>
</ul>
</li>
</ul>
</li>
<li><code>CustomException.class</code>: 자주 발생하는 예외를 별도로 정의한 사용자 정의 예외 클래스<ul>
<li>예외를 표준화하여 프로젝트의 안정성과 유지보수성을 개선</li>
<li><code>RuntimeException</code> 예외 클래스 상속</li>
<li>재사용성 및 코드 가독성 향상<ul>
<li>Ex) <code>CustomNotFoundException</code>, <code>CustomValidateException</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<h3 id="exceptionhandler">ExceptionHandler</h3>
<ul>
<li><strong><code>@RestControllerAdvice</code>를 사용해 프로젝트 전역에서 발생하는 예외 처리</strong></li>
<li><strong><code>CommonResponse</code>를 활용해 에러 응답 형식을 표준화</strong></li>
<li><strong>보안을 위해 발생한 예외 정보를 응답하는 대신 서버 로그로 남김</strong></li>
<li><strong>반드시 상속 관계를 고려해 구체적인 예외(하위 객체)를 먼저 처리</strong></li>
<li><strong>반드시 상속 관계를 고려해 일반적인 예외(상위 객체)를 나중에 처리</strong></li>
</ul>
<hr>
<blockquote>
</blockquote>
<h3 id="customexception">CustomException</h3>
<ul>
<li>자주 발생하는 예외를 별도로 정의한 사용자 정의 예외 클래스<ul>
<li>예외를 표준화하여 프로젝트의 안정성과 유지보수성을 개선</li>
<li><code>RuntimeException</code> 예외 클래스 상속</li>
<li>재사용성 및 코드 가독성 향상<ul>
<li>Ex) <code>CustomNotFoundException</code>, <code>CustomValidateException</code><blockquote>
</blockquote>
<h3 id="customexceptionhandler">CustomExceptionHandler</h3>
</li>
</ul>
</li>
</ul>
</li>
<li>전역적으로 예외를 처리하며, 사용자 정의 오류에 대한 표준화된 응답 제공</li>
<li>사용자 정의 예외를 서버 로그에 남겨 디버깅 및 유지보수 효율성 향상</li>
<li>보안을 위해 상태 코드와 추상적인 예외 메시지를 클라이언트에 전달</li>
</ul>
<blockquote>
</blockquote>
<h3 id="globalexception">GlobalException</h3>
<ul>
<li>프로젝트 전반에서 발생하는 예상치 못한 예외를 포괄적으로 처리하는 상위 예외 클래스</li>
<li><code>Exception</code>: 컴파일 시 발생하는 검사 예외(Checked Exception) 클래스<ul>
<li>컴파일 시 예외 처리 요구, 처리하지 않으면 프로그램이 컴파일되지 않음<ul>
<li>Ex) <code>IOException</code>, <code>SQLException</code></li>
</ul>
</li>
</ul>
</li>
<li><code>RuntimeException</code>: 런타임 시 발생하는 비검사 예외(Unchecked Exception) 클래스<ul>
<li>런타임(실행 중) 시 발생하며, 예외를 처리하지 않으면 프로그램 중단</li>
<li>컴파일 시에는 오류 발생하지 않음, 실행 중 예외 발생 전까지는 프로그램 정상 작동<ul>
<li>Ex) <code>NullPointerException</code>, <code>IllegalArgumentException</code><blockquote>
</blockquote>
<h3 id="globalexceptionhandler">GlobalExceptionHandler</h3>
</li>
</ul>
</li>
</ul>
</li>
<li>전역적으로 예외를 처리하며, 예기치 못한 오류에 대한 표준화된 응답 제공</li>
<li>모든 예외를 서버 로그에 남겨 디버깅 및 유지보수 효율성 향상</li>
<li>보안을 위해 상태 코드와 추상적인 예외 메시지를 클라이언트에 전달</li>
</ul>
<hr>
<blockquote>
</blockquote>
<h3 id="customexception-및-exceptionhandler-구현">CustomException 및 ExceptionHandler 구현</h3>
<ul>
<li><code>CustomException</code> 구현<ul>
<li><code>CustomNotFoundException</code> 구현</li>
<li><code>CustomValidateException</code> 구현</li>
</ul>
</li>
<li><code>CustomExceptionHandler</code> 구현</li>
<li><code>GlobalExceptionHandler</code> 구현</li>
</ul>
<br>

<h2 id="📖-customnotfoundexception">📖 CustomNotFoundException</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.common.exception;

public class CustomNotFoundException extends RuntimeException {

    public CustomNotFoundException() {
        super(&quot;The requested resource could not be found.&quot;);
    }

    public CustomNotFoundException(String message) {
        super(message);
    }

    public CustomNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}</code></pre>
<blockquote>
</blockquote>
<h3 id="customexception-1">CustomException</h3>
<ul>
<li>자주 발생하는 예외를 별도로 정의한 사용자 정의 예외 클래스<ul>
<li>예외를 표준화하여 프로젝트의 안정성과 유지보수성을 개선</li>
<li><code>RuntimeException</code> 예외 클래스 상속</li>
<li>재사용성 및 코드 가독성 향상</li>
</ul>
</li>
</ul>
<br>

<h2 id="📖-customvalidateexception">📖 CustomValidateException</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.common.exception;

public class CustomValidateException extends RuntimeException {

    public CustomValidateException() {
        super(&quot;The request is invalid, validation failed.&quot;);
    }

    public CustomValidateException(String message) {
        super(message);
    }

    public CustomValidateException(String message, Throwable cause) {
        super(message, cause);
    }
}</code></pre>
<blockquote>
</blockquote>
<h3 id="customexception-2">CustomException</h3>
<ul>
<li>자주 발생하는 예외를 별도로 정의한 사용자 정의 예외 클래스<ul>
<li>예외를 표준화하여 프로젝트의 안정성과 유지보수성을 개선</li>
<li><code>RuntimeException</code> 예외 클래스 상속</li>
<li>재사용성 및 코드 가독성 향상</li>
</ul>
</li>
</ul>
<br>

<h2 id="📖-customexceptionhandler">📖 CustomExceptionHandler</h2>
<hr>
<blockquote>
</blockquote>
<h3 id="customexceptionhandler-1">CustomExceptionHandler</h3>
<ul>
<li>전역적으로 예외를 처리하며, 사용자 정의 오류에 대한 표준화된 응답 제공</li>
<li>사용자 정의 예외를 서버 로그에 남겨 디버깅 및 유지보수 효율성 향상</li>
<li>보안을 위해 상태 코드와 추상적인 예외 메시지를 클라이언트에 전달</li>
</ul>
<pre><code class="language-java">package com.companyname.projectname.common.handler;

import com.companyname.projectname.common.dto.CommonResponse;
import com.companyname.projectname.common.exception.CustomNotFoundException;
import com.companyname.projectname.common.exception.CustomValidateException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class CustomExceptionHandler {

    @ExceptionHandler(CustomNotFoundException.class)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; handleCustomNotFoundException(CustomNotFoundException e) {
        log.error(&quot;CustomExceptionHandler CustomNotFoundException occurred: {}&quot;, e.getMessage(), e);
        return ResponseEntity.status(801)
                .body(CommonResponse.error(801));
    }

    @ExceptionHandler(CustomValidateException.class)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; handleCustomValidateException(CustomValidateException e) {
        log.error(&quot;CustomExceptionHandler CustomValidateException occurred: {}&quot;, e.getMessage(), e);
        return ResponseEntity.status(802)
                .body(CommonResponse.error(802));
    }
}</code></pre>
<blockquote>
</blockquote>
<h3 id="exceptionhandler-1">ExceptionHandler</h3>
<ul>
<li><strong><code>@RestControllerAdvice</code>를 사용해 프로젝트 전역에서 발생하는 예외 처리</strong></li>
<li><strong><code>CommonResponse</code>를 활용해 에러 응답 형식을 표준화</strong></li>
<li><strong>보안을 위해 발생한 예외 정보를 응답하는 대신 서버 로그로 남김</strong></li>
<li><strong>반드시 상속 관계를 고려해 구체적인 예외(하위 객체)를 먼저 처리</strong></li>
<li><strong>반드시 상속 관계를 고려해 일반적인 예외(상위 객체)를 나중에 처리</strong></li>
</ul>
<br>

<h2 id="📖-globalexceptionhandler">📖 GlobalExceptionHandler</h2>
<hr>
<blockquote>
</blockquote>
<h3 id="globalexceptionhandler-1">GlobalExceptionHandler</h3>
<ul>
<li>전역적으로 예외를 처리하며, 예기치 못한 오류에 대한 표준화된 응답 제공</li>
<li>모든 예외를 서버 로그에 남겨 디버깅 및 유지보수 효율성 향상</li>
<li>보안을 위해 상태 코드와 추상적인 예외 메시지를 클라이언트에 전달</li>
</ul>
<pre><code class="language-java">package com.companyname.projectname.common.handler;

import com.companyname.projectname.common.dto.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLException;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; handleIllegalArgumentException(IllegalArgumentException e) {
        log.error(&quot;GlobalExceptionHandler IllegalArgumentException occurred: {}&quot;, e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(CommonResponse.error(400));
    }

    @ExceptionHandler(NullPointerException.class)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; handleNullPointerException(NullPointerException e) {
        log.error(&quot;GlobalExceptionHandler NullPointerException occurred: {}&quot;, e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(CommonResponse.error(500));
    }

    @ExceptionHandler(SQLException.class)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; handleSQLException(SQLException e) {
        log.error(&quot;GlobalExceptionHandler SQLException occurred: {}&quot;, e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(CommonResponse.error(500));
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; handleRuntimeException(RuntimeException e) {
        log.error(&quot;GlobalExceptionHandler RuntimeException occurred: {}&quot;, e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(CommonResponse.error(500));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity&lt;CommonResponse&lt;?&gt;&gt; handleGeneralException(Exception e) {
        log.error(&quot;GlobalExceptionHandler General Exception occurred: {}&quot;, e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(CommonResponse.error(500));
    }
}
</code></pre>
<blockquote>
</blockquote>
<h3 id="exceptionhandler-2">ExceptionHandler</h3>
<ul>
<li><strong><code>@RestControllerAdvice</code>를 사용해 프로젝트 전역에서 발생하는 예외 처리</strong></li>
<li><strong><code>CommonResponse</code>를 활용해 에러 응답 형식을 표준화</strong></li>
<li><strong>보안을 위해 발생한 예외 정보를 응답하는 대신 서버 로그로 남김</strong></li>
<li><strong>반드시 상속 관계를 고려해 구체적인 예외(하위 객체)를 먼저 처리</strong></li>
<li><strong>반드시 상속 관계를 고려해 일반적인 예외(상위 객체)를 나중에 처리</strong></li>
</ul>
<br>

<h2 id="📖-postreadserviceimpl">📖 PostReadServiceImpl</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.service;

import com.companyname.projectname.common.exception.CustomNotFoundException;
import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostListResponseDto;
import com.companyname.projectname.post.model.Post;
import com.companyname.projectname.post.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostReadServiceImpl implements PostReadService {

    private final PostRepository postRepository;

    @Override
    public PostDetailResponseDto findById(Long id) {
        Post entity = postRepository.findById(id).orElseThrow(
                () -&gt; new CustomNotFoundException(&quot;Post not found with id: &quot; + id));
        return new PostDetailResponseDto(entity);
    }

    // ... 이하 변경 없음 ...
}</code></pre>
<blockquote>
</blockquote>
<h3 id="유효성-검사-로직-추가">유효성 검사 로직 추가</h3>
<p>예제 코드를 참고하여 필요에 따라 응용하세요.</p>
<ul>
<li><code>throw new CustomNotFoundException();</code></li>
<li><code>throw new CustomValidateException();</code></li>
</ul>
<br>

<h2 id="📖-postwriteserviceimpl">📖 PostWriteServiceImpl</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.service;

import com.companyname.projectname.common.exception.CustomNotFoundException;
import com.companyname.projectname.common.exception.CustomValidateException;
import com.companyname.projectname.post.dto.PostCreateRequestDto;
import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostUpdateRequestDto;
import com.companyname.projectname.post.model.Post;
import com.companyname.projectname.post.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class PostWriteServiceImpl implements PostWriteService {

    private final PostRepository postRepository;

    @Override
    public PostDetailResponseDto create(PostCreateRequestDto requestDto) {
        if (requestDto.getTitle() == null || requestDto.getTitle().isEmpty())
            throw new CustomValidateException(&quot;Post title must not be empty.&quot;);
        if (requestDto.getContent() == null || requestDto.getContent().isEmpty())
            throw new CustomValidateException(&quot;Post content must not be empty.&quot;);

        return new PostDetailResponseDto(postRepository.save(requestDto.toEntity()));
    }

    @Override
    public PostDetailResponseDto update(PostUpdateRequestDto requestDto) {
        Post entity = postRepository.findById(requestDto.getId()).orElseThrow(
                () -&gt; new CustomNotFoundException(&quot;Post not found with id: &quot; + requestDto.getId()));
        return new PostDetailResponseDto(postRepository.save(entity.update(requestDto)));
    }

    @Override
    public void delete(Long id) {
        if (!postRepository.existsById(id))
            throw new CustomNotFoundException(&quot;Cannot delete Post. Post not found with id: &quot; + id);
        postRepository.deleteById(id);
    }
}</code></pre>
<blockquote>
</blockquote>
<h3 id="유효성-검사-로직-추가-1">유효성 검사 로직 추가</h3>
<p>예제 코드를 참고하여 필요에 따라 응용하세요.</p>
<ul>
<li><code>throw new CustomNotFoundException();</code></li>
<li><code>throw new CustomValidateException();</code></li>
</ul>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 게시판 만들기 실습 ⑤ - Controller]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%8B%A4%EC%8A%B5-Controller</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%8B%A4%EC%8A%B5-Controller</guid>
            <pubDate>Thu, 21 Nov 2024 09:48:22 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>

<br>

<h1 id="📌-패키지-구조">📌 패키지 구조</h1>
<hr>
<strong>

<pre><code>├── src
│   ├── main
│   │   ├── java
│   │   │   └── com.companyname.projectname
│   │   │       ├── post
│   │   │       │   ├── controller                // 패키지 추가
│   │   │       │   │   └── PostController.java        // 클래스 추가
│   │   │       │   ├── dto
│   │   │       │   │   ├── PostCreateRequestDto.java
│   │   │       │   │   ├── PostUpdateRequestDto.java
│   │   │       │   │   ├── PostDetailResponseDto.java
│   │   │       │   │   └── PostListResponseDto.java
│   │   │       │   ├── model
│   │   │       │   │   └── Post.java
│   │   │       │   ├── repository
│   │   │       │   │   └── PostRepository.java
│   │   │       │   └── service
│   │   │       │       ├── PostReadService.java
│   │   │       │       ├── PostReadServiceImpl.java
│   │   │       │       ├── PostWriteService.java
│   │   │       │       └── PostWriteServiceImpl.java
│   │   │       └── ProjectNameApplication.java
│   │   └── resources
│   └── test</code></pre></strong>

<br>

<h1 id="📌-controller">📌 Controller</h1>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.controller;

import com.companyname.projectname.post.dto.PostCreateRequestDto;
import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostListResponseDto;
import com.companyname.projectname.post.dto.PostUpdateRequestDto;
import com.companyname.projectname.post.service.PostReadService;
import com.companyname.projectname.post.service.PostWriteService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class PostController {
    private final PostReadService postReadService;
    private final PostWriteService postWriteService;

    @GetMapping(&quot;/api/v1/post/list&quot;)
    public List&lt;PostListResponseDto&gt; getOverviewPosts() {
        return postReadService.findAllLists();
    }

    @GetMapping(&quot;/api/v1/post/list/detail&quot;)
    public List&lt;PostDetailResponseDto&gt; getDetailedPosts() {
        return postReadService.findAllDetails();
    }

    @GetMapping(&quot;/api/v1/post/{id}&quot;)
    public PostDetailResponseDto getPost(@PathVariable Long id) {
        return postReadService.findById(id);
    }

    @PostMapping(&quot;/api/v1/post&quot;)
    public PostDetailResponseDto createPost(@RequestBody PostCreateRequestDto requestDto) {
        return postWriteService.create(requestDto);
    }

    @PutMapping(&quot;/api/v1/post&quot;)
    public PostDetailResponseDto updatePost(@RequestBody PostUpdateRequestDto requestDto) {
        return postWriteService.update(requestDto);
    }

    @DeleteMapping(&quot;/api/v1/post/{id}&quot;)
    public void deletePost(@PathVariable Long id) {
        postWriteService.delete(id);
    }
}</code></pre>
<blockquote>
</blockquote>
<ul>
<li><code>@RestController</code><ul>
<li>API 서버를 구현할 때 주로 사용하며,
컨트롤러 클래스를 RESTful 웹 서비스의 엔드포인트로 만들어줌</li>
<li><code>@Controller</code>와 <code>@ResponseBody</code>를 결합한 형태로, 반환값을 JSON 또는 XML 형태로
바로 변환하여 클라이언트에 전달</li>
</ul>
</li>
<li><code>@RequiredArgsConstructor</code><ul>
<li>필드 주입 대신 생성자 주입으로 PostService 자동으로 의존성 주입</li>
<li>Spring DI(Dependency Injection) 컨테이너에서 안전하게 의존성 관리</li>
<li>선언된 <code>final</code> 필드는 생성자에서 자동으로 초기화</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li><code>@GetMapping</code>, <code>@PostMapping</code>, <code>@PutMapping</code>, <code>@DeleteMapping</code><ul>
<li>HTTP 메서드별 요청을 처리하는 매핑 어노테이션<ul>
<li><code>@GetMapping</code>: GET 요청 처리, 주로 조회 작업에 사용</li>
<li><code>@PostMapping</code>: POST 요청 처리, 주로 생성 작업에 사용</li>
<li><code>@PutMapping</code>: PUT 요청 처리, 주로 업데이트 작업에 사용</li>
<li><code>@DeleteMapping</code>: DELETE 요청 처리, 주로 삭제 작업에 사용</li>
</ul>
</li>
<li>RESTful API를 구현할 때 간결하고 가독성 높은 코드 작성 가능</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li><code>@PathVariable</code><ul>
<li>URL 경로에 포함된 변수를 매핑하는 데 사용</li>
<li>주로 RESTful API에서 자원 식별자(ID) 등을 처리할 때 사용</li>
</ul>
</li>
<li><code>@RequestBody</code><ul>
<li>HTTP 요청의 본문(Body)을 Java 객체로 변환하여 매핑하는 데 사용</li>
<li>주로 POST, PUT 요청에서 JSON 데이터를 객체로 변환할 때 사용</li>
</ul>
</li>
</ul>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 게시판 만들기 실습 ④ - Service, Repository]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-Service-Repository</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-Service-Repository</guid>
            <pubDate>Thu, 21 Nov 2024 09:46:28 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>

<br>

<h1 id="📌-패키지-구조">📌 패키지 구조</h1>
<hr>
<strong>

<pre><code class="language-bash">├── src
│   ├── main
│   │   ├── java
│   │   │   └── com.companyname.projectname
│   │   │       ├── post
│   │   │       │   ├── dto
│   │   │       │   │   ├── PostCreateRequestDto.java
│   │   │       │   │   ├── PostUpdateRequestDto.java
│   │   │       │   │   ├── PostDetailResponseDto.java
│   │   │       │   │   └── PostListResponseDto.java
│   │   │       │   ├── model
│   │   │       │   │   └── Post.java
│   │   │       │   ├── repository                    // 패키지 추가
│   │   │       │   │   └── PostRepository.java            // 인터페이스 추가
│   │   │       │   └── service                        // 패키지 추가
│   │   │       │       ├── PostReadService.java            // 인터페이스 추가
│   │   │       │       ├── PostReadServiceImpl.java        // 클래스 추가
│   │   │       │       ├── PostWriteService.java        // 인터페이스 추가
│   │   │       │       └── PostWriteServiceImpl.java    // 클래스 추가
│   │   │       └── ProjectNameApplication.java
│   │   └── resources
│   └── test</code></pre>
</strong>

<br>

<h1 id="📌-repository">📌 Repository</h1>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.repository;

import com.companyname.projectname.post.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface PostRepository extends JpaRepository&lt;Post, Long&gt; {
    // 1. 이름 기반 메서드 (쿼리 메서드)

    // 기본 메서드
    List&lt;Post&gt; findByTitle(String title);
    List&lt;Post&gt; findByContent(String content);
    List&lt;Post&gt; findByTitleContaining(String title);
    List&lt;Post&gt; findByContentContaining(String content);
    List&lt;Post&gt; findByTitleContainingIgnoreCase(String title);
    List&lt;Post&gt; findByContentContainingIgnoreCase(String content);

    // 복합 메서드
    List&lt;Post&gt; findByTitleAndContent(String title, String content);
    List&lt;Post&gt; findByTitleContainingOrContentContaining(String title, String content);
    List&lt;Post&gt; findByTitleContainingAndContentContaining(String title, String content);
    List&lt;Post&gt; findByTitleContainingIgnoreCaseOrContentContainingIgnoreCase(String title, String content);
    List&lt;Post&gt; findByTitleContainingIgnoreCaseAndContentContainingIgnoreCase(String title, String content);

    // 날짜 기반 메서드 (생성 일자 기준)
    List&lt;Post&gt; findByCreatedDateAfter(LocalDateTime date);
    List&lt;Post&gt; findByCreatedDateBefore(LocalDateTime date);
    List&lt;Post&gt; findByCreatedDateBetween(LocalDateTime sDate, LocalDateTime eDate);

    // 날짜 기반 메서드 (수정 일자 기준)
    List&lt;Post&gt; findByUpdatedDateAfter(LocalDateTime date);
    List&lt;Post&gt; findByUpdatedDateBefore(LocalDateTime date);
    List&lt;Post&gt; findByUpdatedDateBetween(LocalDateTime sDate, LocalDateTime eDate);
}</code></pre>
<ul>
<li><code>JPA</code>는 <code>Entity</code>  클래스의 필드명을 기준으로, 메서드명을 통해 JPA 원시 쿼리를 자동으로 생성</li>
<li>필요에 따라 메서드를 추가하되, 반드시 메서드명, 매개변수명에 주의할 것</li>
</ul>
<br>

<h1 id="📌-service">📌 Service</h1>
<hr>
<blockquote>
</blockquote>
<p><em><strong>확장성과 유지보수성을 고려하여 <code>Interface</code>로 구현하였으며,
<code>@Transactional</code> 조회 성능 최적화를 위해 읽기, 쓰기 작업을 분리</strong></em></p>
<br>

<h2 id="📖-postreadservice">📖 PostReadService</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.service;

import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostListResponseDto;

import java.time.LocalDateTime;
import java.util.List;

public interface PostReadService {
    PostDetailResponseDto findById(Long id);
    List&lt;PostListResponseDto&gt; findAllLists();
    List&lt;PostDetailResponseDto&gt; findAllDetails();

    // 예시, 필요한 경우 작성
    List&lt;PostListResponseDto&gt; findByTitle(String title);
    List&lt;PostListResponseDto&gt; findByContent(String content);
    List&lt;PostListResponseDto&gt; findByTitleContaining(String title);
    List&lt;PostListResponseDto&gt; findByContentContaining(String content);
    List&lt;PostListResponseDto&gt; findByTitleContainingIgnoreCase(String title);
    List&lt;PostListResponseDto&gt; findByContentContainingIgnoreCase(String content);

    List&lt;PostListResponseDto&gt; findByTitleAndContent(String title, String content);
    List&lt;PostListResponseDto&gt; findByTitleContainingOrContentContaining(String title, String content);
    List&lt;PostListResponseDto&gt; findByTitleContainingAndContentContaining(String title, String content);
    List&lt;PostListResponseDto&gt; findByTitleContainingIgnoreCaseOrContentContainingIgnoreCase(String title, String content);
    List&lt;PostListResponseDto&gt; findByTitleContainingIgnoreCaseAndContentContainingIgnoreCase(String title, String content);

    List&lt;PostListResponseDto&gt; findByCreatedDateAfter(LocalDateTime date);
    List&lt;PostListResponseDto&gt; findByCreatedDateBefore(LocalDateTime date);
    List&lt;PostListResponseDto&gt; findByCreatedDateBetween(LocalDateTime sDate, LocalDateTime eDate);

    List&lt;PostListResponseDto&gt; findByUpdatedDateAfter(LocalDateTime date);
    List&lt;PostListResponseDto&gt; findByUpdatedDateBefore(LocalDateTime date);
    List&lt;PostListResponseDto&gt; findByUpdatedDateBetween(LocalDateTime sDate, LocalDateTime eDate);
}</code></pre>
<blockquote>
</blockquote>
<ul>
<li><code>Interface</code><ul>
<li>클래스 간 공통된 메서드의 규격을 정의</li>
<li>다형성을 활용하여 유연하고 확장 가능한 코드 설계 가능</li>
<li>메서드 선언부만 존재하며, 구현부는 구현 클래스에서 정의</li>
<li>다중 상속을 지원하여 다양한 인터페이스를 한 클래스에서 구현 가능</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li><code>Interface</code> 주의할 점<ul>
<li>구현 클래스에서 반드시 메서드를 <code>@Override</code>해야 함</li>
<li>다중 상속 시 중복된 메서드가 존재하는 경우 충돌에 주의</li>
<li>구현해야할 기능의 정의는 결정되었으나, 로직은 결정되지 않은 경우 사용</li>
<li>필요 이상으로 많은 메서드를 정의하면 구현 클래스의 복잡도를 증가시킬 수 있음</li>
</ul>
</li>
</ul>
<br>

<h2 id="📖-postreadserviceimpl">📖 PostReadServiceImpl</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.service;

import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostListResponseDto;
import com.companyname.projectname.post.model.Post;
import com.companyname.projectname.post.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostReadServiceImpl implements PostReadService {

    private final PostRepository postRepository;

    @Override
    public PostDetailResponseDto findById(Long id) {
        Post entity = postRepository.findById(id).orElseThrow(
                () -&gt; new IllegalArgumentException(&quot;Post not found with id: &quot; + id));
        return new PostDetailResponseDto(entity);
    }

    @Override
    public List&lt;PostListResponseDto&gt; findAllLists() {
        return postRepository.findAll().stream().map(PostListResponseDto::new).toList();
    }

    @Override
    public List&lt;PostDetailResponseDto&gt; findAllDetails() {
        return postRepository.findAll().stream().map(PostDetailResponseDto::new).toList();
    }

    @Override
    public List&lt;PostListResponseDto&gt; findByTitle(String title) {
        return postRepository.findByTitle(title).stream().map(PostListResponseDto::new).toList();
    }

    @Override
    public List&lt;PostListResponseDto&gt; findByContent(String content) {
        return postRepository.findByContent(content).stream().map(PostListResponseDto::new).toList();
    }

    // ... 중략 ...
}</code></pre>
<blockquote>
<ul>
<li><code>@Transactional(readOnly = true)</code></li>
</ul>
</blockquote>
<ul>
<li><code>Spring</code> 제공 어노테이션, 조회 전용 메서드에 설정하여 성능 최적화를 도모
쓰기 작업을 막고 읽기 전용으로 동작, 일부 데이터베이스에서 캐시를 활용하도록 도움</li>
<li>트랜잭션 범위를 설정하고, 데이터베이스 작업의 일관성과 원자성을 보장</li>
<li>데이터 수정/삭제/저장 작업에서 예외 발생 시, 롤백을 자동으로 처리<ul>
<li>클래스 레벨에 선언하면 해당 클래스의 모든 메서드에 트랜잭션 속성 적용</li>
<li>메서드 레벨에 선언하면 해당 메서드에만 트랜잭션 속성 적용</li>
<li>클래스 레벨과 메서드 레벨에 모두 선언하면
메서드 레벨 설정이 클래스 레벨 설정을 재정의</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li><code>@RequiredArgsConstructor</code><ul>
<li>필드 주입 대신 생성자 주입으로 PostRepository 자동으로 의존성 주입</li>
<li>Spring DI(Dependency Injection) 컨테이너에서 안전하게 의존성 관리</li>
<li>선언된 <code>final</code> 필드는 생성자에서 자동으로 초기화</li>
</ul>
</li>
<li><code>IllegalArgumentException</code><ul>
<li>데이터가 없을 경우, <strong>orElseThrow()</strong>를 사용하여 명시적으로 예외를 던짐</li>
<li>추후 별도의 예외 클래스를 생성하여 더 구체적인 예외 처리가 가능하도록 변경</li>
</ul>
</li>
</ul>
<br>

<h2 id="📖-postwriteservice">📖 PostWriteService</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.service;

import com.companyname.projectname.post.dto.PostCreateRequestDto;
import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostUpdateRequestDto;

public interface PostWriteService {
    PostDetailResponseDto create(PostCreateRequestDto requestDto);
    PostDetailResponseDto update(PostUpdateRequestDto requestDto);
    void delete(Long id);
}</code></pre>
<blockquote>
</blockquote>
<ul>
<li><code>Interface</code><ul>
<li>클래스 간 공통된 메서드의 규격을 정의</li>
<li>다형성을 활용하여 유연하고 확장 가능한 코드 설계 가능</li>
<li>메서드 선언부만 존재하며, 구현부는 구현 클래스에서 정의</li>
<li>다중 상속을 지원하여 다양한 인터페이스를 한 클래스에서 구현 가능</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li><code>Interface</code> 주의할 점<ul>
<li>구현 클래스에서 반드시 메서드를 <code>@Override</code>해야 함</li>
<li>다중 상속 시 중복된 메서드가 존재하는 경우 충돌에 주의</li>
<li>구현해야할 기능의 정의는 결정되었으나, 로직은 결정되지 않은 경우 사용</li>
<li>필요 이상으로 많은 메서드를 정의하면 구현 클래스의 복잡도를 증가시킬 수 있음</li>
</ul>
</li>
</ul>
<br>

<h2 id="📖-postwriteserviceimpl">📖 PostWriteServiceImpl</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.service;

import com.companyname.projectname.post.dto.PostCreateRequestDto;
import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostUpdateRequestDto;
import com.companyname.projectname.post.model.Post;
import com.companyname.projectname.post.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class PostWriteServiceImpl implements PostWriteService {

    private final PostRepository postRepository;

    @Override
    public PostDetailResponseDto create(PostCreateRequestDto requestDto) {
        return new PostDetailResponseDto(postRepository.save(requestDto.toEntity()));
    }

    @Override
    public PostDetailResponseDto update(PostUpdateRequestDto requestDto) {
        Post entity = postRepository.findById(requestDto.getId()).orElseThrow(
                () -&gt; new IllegalArgumentException(&quot;Post not found with id: &quot; + requestDto.getId()));
        return new PostDetailResponseDto(postRepository.save(entity.update(requestDto)));
    }

    @Override
    public void delete(Long id) {
        postRepository.deleteById(id);
    }
}</code></pre>
<blockquote>
<ul>
<li><code>@Transactional</code></li>
</ul>
</blockquote>
<ul>
<li><code>Spring</code> 제공 어노테이션</li>
<li>트랜잭션 범위를 설정하고, 데이터베이스 작업의 일관성과 원자성을 보장</li>
<li>데이터 수정/삭제/저장 작업에서 예외 발생 시, 롤백을 자동으로 처리<ul>
<li>클래스 레벨에 선언하면 해당 클래스의 모든 메서드에 트랜잭션 속성 적용</li>
<li>메서드 레벨에 선언하면 해당 메서드에만 트랜잭션 속성 적용</li>
<li>클래스 레벨과 메서드 레벨에 모두 선언하면
메서드 레벨 설정이 클래스 레벨 설정을 재정의</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li><code>@RequiredArgsConstructor</code><ul>
<li>필드 주입 대신 생성자 주입으로 PostRepository 자동으로 의존성 주입</li>
<li>Spring DI(Dependency Injection) 컨테이너에서 안전하게 의존성 관리</li>
<li>선언된 <code>final</code> 필드는 생성자에서 자동으로 초기화</li>
</ul>
</li>
<li><code>IllegalArgumentException</code><ul>
<li>데이터가 없을 경우, <strong>orElseThrow()</strong>를 사용하여 명시적으로 예외를 던짐</li>
<li>추후 별도의 예외 클래스를 생성하여 더 구체적인 예외 처리가 가능하도록 변경</li>
</ul>
</li>
</ul>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 게시판 만들기 실습 ② - MySQL 연동]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-MySQL-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-MySQL-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Thu, 21 Nov 2024 08:32:32 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>

<br>

<h1 id="📌-buildgrade">📌 build.grade</h1>
<hr>
<p><code>build.grade</code> 의존성 라이브러리 추가</p>
<pre><code class="language-gradle">dependencies {
    ...
    implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;    // 추가
    runtimeOnly &#39;com.mysql:mysql-connector-j&#39;                                // 추가
    ...
}</code></pre>
<br>

<h1 id="📌-applicationproperties">📌 application.properties</h1>
<hr>
<br>

<h2 id="📖-applicationproperties">📖 application.properties</h2>
<hr>
<pre><code class="language-properties"># MySQL Settings
spring.datasource.url=jdbc:mysql://localhost:3306/myschema?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Seoul
spring.datasource.username=[MySQL계정이름]
spring.datasource.password=[MySQL비밀번호]
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA Settings
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql: true</code></pre>
<ul>
<li><code>spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect</code><ul>
<li>자동 감지가 실패하거나, 올바르지 않은 Dialect가 선택될 때 명시적으로 설정</li>
<li>특정 데이터베이스 버전을 위한 Dialect를 지정하고 싶을 때 명시적으로 설정<ul>
<li>MySQL 5.x: org.hibernate.dialect.MySQLDialect</li>
<li>MySQL 8.x: org.hibernate.dialect.MySQL8Dialect</li>
</ul>
</li>
</ul>
</li>
<li><code>spring.jpa.properties.hibernate.format_sql=true</code><ul>
<li>Hibernate가 실행하는 SQL 쿼리를 읽기 쉬운 포맷으로 출력</li>
</ul>
</li>
<li><code>spring.jpa.hibernate.ddl-auto=update</code>: Spring Boot Application에서<ul>
<li>Hibernate의 DDL(Data Definition Language) 생성 및 동작 방식을 제어하는 설정</li>
<li><strong><u>데이터베이스 스키마(테이블, 속성 등)의 상태를, Entity 클래스와 동기화</u></strong><ul>
<li><code>none</code>: 데이터베이스 스키마 변경 작업을 수행하지 않음. 수동 관리 필요.</li>
<li><code>validate</code>: 데이터베이스 스키마와 Entity 불일치 시 애플리케이션 실행 오류</li>
<li><code>update</code>: Entity에 맞춰 데이터베이스 스키마 업데이트 (운영 환경 비추천)</li>
<li><code>create</code>: 애플리케이션 실행 시 스키마 삭제 후 새로 생성 (기존 데이터 삭제)</li>
<li><code>create-drop</code>: 애플리케이션 실행 시 스키마 생성, 종료 시 삭제 (테스트 환경에 적합)</li>
</ul>
</li>
</ul>
</li>
<li><code>spring.jpa.show-sql=true</code><ul>
<li>Hibernate가 실행하는 SQL 쿼리를 콘솔(로그)에 출력하는 설정</li>
<li>애플리케이션에서 <strong><u>JPA를 통해 데이터베이스와 상호작용할 때,
생성되고 실행되는 SQL 원시 쿼리를 콘솔(로그)에 표시</u></strong></li>
</ul>
</li>
</ul>
<br>

<h2 id="📖-mysql-jdbc-설정-옵션">📖 MySQL JDBC 설정 옵션</h2>
<hr>
<p><code>application.properties</code></p>
<pre><code>spring.datasource.url=jdbc:mysql://[IP]:[포트번호]/[DB스키마]?
useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Seoul</code></pre><ul>
<li><code>useUnicode=true</code><ul>
<li>데이터베이스가 유니코드(UTF-8)로 데이터를 읽고 쓰도록 설정</li>
<li>한글 데이터를 올바르게 처리하기 위해 필수</li>
</ul>
</li>
<li><code>characterEncoding=utf8</code><ul>
<li>데이터베이스와의 통신에 사용할 문자 인코딩을 UTF-8로 설정</li>
<li>한글 데이터를 올바르게 처리하기 위해 필수</li>
</ul>
</li>
<li><code>serverTimezone=Asia/Seoul</code><ul>
<li>서버의 시간대를 대한민국 표준시간(Asia/Seoul)으로 설정</li>
<li>시간 관련 연산에서 정확한 데이터를 보장합니다.</li>
</ul>
</li>
<li><code>allowPublicKeyRetrieval=true</code><ul>
<li>MySQL 8.x 버전 이상에서 Public Key Retrieval 문제 방지를 위해 사용</li>
<li>MySQL 8.x 버전 이상에서 보안 정책에 따른 옵션으로, 로컬 개발 환경에서 사용</li>
</ul>
</li>
<li><code>useSSL=false</code><ul>
<li>SSL 비활성화 (로컬/테스트 환경에서 사용)</li>
<li>로컬 또는 테스트 환경에서는 SSL 설정이 필요하지 않은 경우가 많아 보통 비활성화</li>
</ul>
</li>
</ul>
<br>

<h1 id="📌-mysql">📌 MySQL</h1>
<hr>
<br>

<h2 id="📖-mysql-connection-instance-생성">📖 MySQL Connection Instance 생성</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/6d5c56c1-5539-435c-aeeb-8c1cd0d96d85/image.png" alt="">
<img src="https://velog.velcdn.com/images/sihoon_cho/post/2a3f8c21-0a21-4f36-8580-4aecfabe0b19/image.png" alt=""></p>
<blockquote>
</blockquote>
<ul>
<li>Connection Name: <code>ProjectName</code></li>
<li><code>Store in Vault ...</code> - <code>root</code> - <code>1234</code> - <code>OK</code><ul>
<li>User: <code>root</code></li>
<li>Password: <code>1234</code></li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/6b334bc5-7e01-49ea-9df3-8e91f7ec54aa/image.png" alt="">
<img src="https://velog.velcdn.com/images/sihoon_cho/post/6943aac1-66bb-4ef6-8f30-14415a7cb0ac/image.png" alt="">
<img src="https://velog.velcdn.com/images/sihoon_cho/post/f61d319e-2613-4638-bcd8-b1702b33f030/image.png" alt=""></p>
<blockquote>
</blockquote>
<ul>
<li><code>Test Connection</code> - <code>OK</code> - <code>OK</code></li>
</ul>
<br>

<h2 id="📖-mysql-schema-생성">📖 MySQL Schema 생성</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/f11b692a-d9cd-4749-abd6-4e54e0c87e1b/image.png" alt="">
<img src="https://velog.velcdn.com/images/sihoon_cho/post/46ac582e-a78b-4121-8767-446af2ca6c83/image.png" alt="">
<img src="https://velog.velcdn.com/images/sihoon_cho/post/db1c8f4a-1eb0-4dfc-8fcd-22ce7ecf0ea0/image.png" alt="">
<img src="https://velog.velcdn.com/images/sihoon_cho/post/71f1ec93-5eab-460f-bbb3-1e6f8b011da7/image.png" alt="">
<img src="https://velog.velcdn.com/images/sihoon_cho/post/4d991735-daa2-4cc7-926e-acec0501d1eb/image.png" alt=""></p>
<blockquote>
</blockquote>
<ul>
<li><code>ProjectName Instance</code> 연결 (클릭)</li>
<li><code>Create a new schema in the connected server</code></li>
<li><code>Name: myschema</code> - <code>Apply</code> - <code>Apply</code> - <code>Finish</code></li>
</ul>
<br>

<h2 id="📖-mysql-connection-instance-schema-설정">📖 MySQL Connection Instance Schema 설정</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/58e207d4-903a-4195-b053-453c100cdff4/image.png" alt="">
<img src="https://velog.velcdn.com/images/sihoon_cho/post/2ad31da0-fb6a-4724-9477-c7a8bf208bda/image.png" alt="">
<img src="https://velog.velcdn.com/images/sihoon_cho/post/764a1eef-d340-433a-acb7-94840e4aea9e/image.png" alt=""></p>
<blockquote>
</blockquote>
<ul>
<li><code>ProjectName Instance</code> - <code>마우스 우클릭</code> - <code>Edit Connection</code></li>
<li><code>Default Schema: myschema</code></li>
<li><code>Test Connection</code> - <code>OK</code></li>
</ul>
<br>

<h2 id="📖-mysql-연동-결과-확인">📖 MySQL 연동 결과 확인</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/8937f281-620a-42d6-ad1f-9b8a249655a7/image.png" alt=""></p>
<blockquote>
</blockquote>
<ul>
<li><strong>데이터베이스에 다시 접속했을 때, <code>myschema</code> 가 굵은 글씨로 표시되어야 합니다.</strong></li>
</ul>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 게시판 만들기 실습 ① - 프로젝트 생성]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Thu, 21 Nov 2024 06:57:41 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>

<br>

<h1 id="📌-spring-initializr">📌 Spring Initializr</h1>
<hr>
<p><strong>아래의 공식 사이트에 접속한 후, 아래와 같이 내용을 입력해주세요.</strong></p>
<ul>
<li><a href="https://start.spring.io/">[Spring Initializr]</a> <a href="https://start.spring.io/">https://start.spring.io/</a></li>
</ul>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/8009f161-0c99-406e-9751-b4f1c2bcbf28/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/6791bb0d-f027-4e64-91c9-d644d65f013e/image.png" alt=""></p>
<br>

<h2 id="📖-입력-내용">📖 입력 내용</h2>
<hr>
<ol>
<li>Project : <code>Gradle - Groovy</code></li>
<li>Language : <code>Java</code></li>
<li>Spring Boot : <code>3.3.5</code>, 버전 뒤에 아무 것도 붙지 않은 것 중, 가장 최신 버전 선택</li>
<li>Project Metadata :<ul>
<li>Group : <code>com.companyname</code></li>
<li>Artifact : <code>ProjectName</code></li>
<li>Name : <code>ProjectName</code></li>
<li>Description : <code>FreeBoard Project for Spring Boot</code></li>
<li>Package name : <code>com.companyname.projectname</code></li>
<li>Packaging : <code>Jar</code></li>
<li>Java : <code>17</code></li>
</ul>
</li>
<li>Dependencies <code>ADD DEPENDENCIES</code><ul>
<li><code>Lombok</code></li>
<li><code>Spring Web</code></li>
<li><code>Spring Boot Dev Tools</code></li>
</ul>
</li>
<li><code>GENERATE</code></li>
<li>다운로드된 파일 압축 해제 후, 인텔리제이에서 프로젝트 열기</li>
</ol>
<br>

<h2 id="📖-입력-내용-설명">📖 입력 내용 설명</h2>
<hr>
<ol>
<li>Project : 빌드 도구 선택 <ul>
<li>Gradle - Groovy : Groovy DSL을 사용하는 Gradle 빌드 도구 <code>build.gradle</code></li>
<li>Gradle - Kotlin : Kotlin DSL을 사용하는 Gradle 빌드 도구 <code>build.gradle.kts</code></li>
<li>Maven : XML 기반의 정형화된 빌드 도구 <code>pom.xml</code></li>
</ul>
</li>
<li>Language : 프로그래밍 언어 선택 </li>
<li>Spring Boot : 버전 뒤에 아무 것도 붙지 않은 것 중, 가장 최신 버전 선택<ul>
<li>SNAPSHOT : 아직 개발이 완료되지 않은 버전</li>
<li>M(Milestone) : 개발은 완료되었으나, 아직 기능 개선 또는 버그 수정중인 버전</li>
<li>RC(Release Candidate) : 기능 개선과 버그 수정이 완료되었으나,
최종적으로 릴리즈되지는 않은 버전</li>
</ul>
</li>
<li>Project Metadata : 프로젝트 정보 입력<ul>
<li><code>Group</code> : 기업명 혹은 개발자명, 보통 기업 도메인 명을 역순으로 입력<ul>
<li>ex) naver.com -&gt; com.naver</li>
<li>ex) hong-gildong.com -&gt; com.hong-gildong</li>
</ul>
</li>
<li><code>Artifact</code> : 빌드 결과물 이름 (수동 입력)</li>
<li><code>Name</code> : 프로젝트명 (Artifact를 수정하면 자동으로 수정됨)</li>
<li><code>Description</code> : 프로젝트에 대한 간략한 설명 (필요시 입력)</li>
<li><code>Package name</code> : 프로젝트에 생성할 패키지 설정 (일부 수정)<ul>
<li>패키지명은 모두 소문자를 사용하는 것을 권장함</li>
<li>ex) com.companyname.ProjectName -&gt; com.companyname.projectname</li>
</ul>
</li>
<li>Packaging : 배포를 위한 프로젝트 압축 방법 선택 </li>
<li>Java : 개발환경 및 배포환경 JDK 버전 선택<ul>
<li>본 게시글 작성일(2024.11.22) 기준 최신 LTS 버전: JDK 21</li>
<li>정석적으로는, Java 지원 기간이 긴 최신 LTS 버전 선택 권장</li>
<li>현실적으로는, 구버전 호환성을 고려해 구 JDK LTS 버전 선택</li>
<li>Spring Boot 3.x 부터는 Java 17 이상 지원 -&gt; <code>JDK 17 권장</code><ul>
<li>Spring Boot 2.x 지원 종료 -&gt; Java 8 지원 종료</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Dependencies : 프로젝트에 필요한 의존성 라이브러리 선택<ul>
<li><code>Lombok</code>: 어노테이션 기반 반복적인 코드(보일러플레이트 코드) 생성 도구 제공</li>
<li><code>Spring Web</code>: RESTful Web Application &amp; MVC 기반 웹 서비스 개발 기본 기능 지원</li>
<li><code>Spring Boot Dev Tools</code>: 자동 재시작 및 캐싱 비활성화 등 개발 편의성 기능 제공</li>
</ul>
</li>
</ol>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 2024 게시판 만들기 실습 ③ - Entity, DTO]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-Entity-DTO</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-2024-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0-Entity-DTO</guid>
            <pubDate>Thu, 21 Nov 2024 06:11:58 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre><hr>
</strong>

<blockquote>
<p><strong>본 예제에서는 <code>Lombok</code>과 <code>JPA</code>를 사용한 실습을 설명하고 있으므로,</strong>
<strong>만약 설치되어있지 않다면 미리 세팅하고 오셔야 오류없이 실행됩니다.</strong></p>
</blockquote>
<br>

<h1 id="📌-패키지-구조">📌 패키지 구조</h1>
<hr>
<strong>

<pre><code class="language-bash">├── src
│   ├── main
│   │   ├── java
│   │   │   └── com.companyname.projectname
│   │   │       ├── post                            // 패키지 추가
│   │   │       │   ├── dto                        // 패키지 추가
│   │   │       │   │   ├── PostCreateRequestDto.java    // 클래스 추가
│   │   │       │   │   ├── PostUpdateRequestDto.java    // 클래스 추가
│   │   │       │   │   ├── PostDetailResponseDto.java    // 클래스 추가
│   │   │       │   │   └── PostListResponseDto.java        // 클래스 추가
│   │   │       │   └── model                    // 패키지 추가
│   │   │       │       └── Post.java                    // 클래스 추가
│   │   │       └── ProjectNameApplication.java
│   │   └── resources
│   └── test</code></pre>
</strong>

<br>

<h1 id="📌-entity">📌 Entity</h1>
<hr>
<p><strong>사실 <code>Entity</code>는 특별히 대단할 것이 없습니다.</strong></p>
<blockquote>
<p><strong><code>Entity</code>는 테이블 정의이자 데이터의 단위인데</strong>
<em><strong>&#39;<code>Java</code>에서는 이것을 <code>Class</code>라고 합니다.&#39;</strong></em></p>
</blockquote>
<p><strong>Java를 좀 공부해보셨던 분들이라면, 이 말을 듣고 아마 이런 생각을 하셨을겁니다.</strong></p>
<blockquote>
<p><em><strong>&quot;아, &#39;데이터의 정의`는 &#39;Class&#39; 였지!&quot;</strong></em></p>
</blockquote>
<br>

<h2 id="📖-postjava">📖 Post.java</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.model;

import com.companyname.projectname.post.dto.PostRequestDto;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String content;

    @CreationTimestamp
    private LocalDateTime createdDate;
    @UpdateTimestamp
    private LocalDateTime updatedDate;

    // Method, for updating data
    public Post update(PostRequestDto requestDto) {
        this.title = requestDto.getTitle();
        this.content = requestDto.getContent();
        return this;
    }
}</code></pre>
<blockquote>
</blockquote>
<ul>
<li><code>@Entity</code>: 이 클래스가 JPA 엔티티임을 명시</li>
<li><code>@Getter</code>: 클래스의 모든 필드에 대해 Getter 메서드를 자동 생성</li>
<li><code>@Builder</code>: 빌더 패턴을 통해 객체 생성 지원</li>
<li><code>@NoArgsConstructor</code>: 매개변수가 없는 기본 생성자 자동 생성</li>
<li><code>@AllArgsConstructor</code>: 모든 필드를 매개변수로 갖는 생성자 자동 생성
변수명을 기준으로 매개변수를 필드에 자동으로 초기화</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li><code>@Id</code>: 해당 필드가 엔티티의 기본 키(Primary Key)임을 지정</li>
<li><code>@GeneratedValue</code>: 데이터베이스에서 기본 키가 자동으로 생성되도록 설정<ul>
<li><code>(strategy = GenerationType.IDENTITY)</code>:
데이터베이스의 <code>AUTO_INCREMENT</code> 기능을 사용하여 기본 키를 생성</li>
</ul>
</li>
<li><code>@CreationTimestamp</code>: 엔티티가 생성될 때 자동으로 현재 시간을 설정</li>
<li><code>@UpdateTimestamp</code>: 엔티티가 업데이트될 때 자동으로 현재 시간을 설정</li>
</ul>
<p><em><strong>각각의 <code>@Annotation</code>은 이를 사용하거나, 사용하지 말아야 할 이유가 존재합니다.</strong></em></p>
<ul>
<li><code>@Builder</code>는 추후 <code>RequestDto</code> -&gt; <code>Entity</code> 클래스로 변환하는데 사용한다.</li>
<li>데이터베이스와 직접 연결되는 엔티티는, <code>@Setter</code> 사용을 지양한다.</li>
<li>수정이 필요한 경우 필요한 데이터에 대해서 메서드로 처리한다.</li>
<li><code>update()</code> 메서드는 매개변수가 <code>RequestDto</code>임에 주의한다.<ul>
<li>아직 구현되지 않았다면 <code>RequestDto</code>부터 구현한다.</li>
</ul>
</li>
</ul>
<p><code>@Annotation</code>에 대한 더 자세한 정보는 구글링을 통해 알아보시기 바랍니다.
검색 키워드는 <strong><code>Lombok Annotation</code>, <code>JPA Annotation</code></strong>입니다.</p>
<br>

<h1 id="📌-requestdto">📌 RequestDTO</h1>
<hr>
<blockquote>
<p><em><strong>각각의 <code>@Annotation</code>은 이를 사용하거나, 사용하지 말아야 할 이유가 존재합니다.</strong></em></p>
</blockquote>
<blockquote>
</blockquote>
<ul>
<li><p><code>@Data</code>: 간단한 데이터 클래스 작성을 위한 종합적인 기능 제공</p>
<ul>
<li><code>@Getter</code>, <code>@Setter</code>, <code>@ToString</code> 포함</li>
<li><code>@EqualsAndHashCode</code>, <code>@RequiredArgsConstructor</code> 포함</li>
</ul>
</li>
<li><p><code>@Builder</code>: 빌더 패턴을 통해 객체 생성 지원</p>
</li>
<li><p><code>@NoArgsConstructor</code>: 매개변수가 없는 기본 생성자 자동 생성</p>
</li>
<li><p><code>@AllArgsConstructor</code>: 모든 필드를 매개변수로 갖는 생성자 자동 생성
변수명을 기준으로 매개변수를 필드에 자동으로 초기화</p>
</li>
<li><p><code>DTO</code>는 비교적 데이터 수정에 관대하므로, <code>@Data</code>를 사용해 중복 코드를 감소</p>
</li>
<li><p><code>toEntity()</code> 메서드는 <code>Post.java</code> 엔티티의 <code>@Builder</code> 패턴을 사용해,
<code>RequestDto</code> -&gt; <code>Entity</code> 클래스로 변환한다.</p>
</li>
</ul>
<br>

<h2 id="📖-postcreaterequestdtojava">📖 PostCreateRequestDto.java</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.dto;

import com.companyname.projectname.post.model.Post;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostCreateRequestDto {
    private String title;
    private String content;

    public Post toEntity() {
        return Post.builder()
                .title(this.title)
                .content(this.content)
                .build();
    }
}</code></pre>
<blockquote>
<p><em><strong>데이터베이스에 아직 데이터가 없는 상태이므로 <code>Long id</code>를 기입하지 않습니다.</strong></em></p>
</blockquote>
<br>

<h2 id="📖-postupdaterequestdtojava">📖 PostUpdateRequestDto.java</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.dto;

import com.companyname.projectname.post.model.Post;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostUpdateRequestDto {
    private Long id;
    private String title;
    private String content;

    public Post toEntity() {
        return Post.builder()
                .id(this.id)
                .title(this.title)
                .content(this.content)
                .build();
    }
}</code></pre>
<blockquote>
<p><em><strong>데이터베이스에 존재하는 데이터를 수정하는 것이므로, <code>Long id</code>를 기입합니다.</strong></em></p>
</blockquote>
<br>


<h1 id="📌-responsedto">📌 ResponseDTO</h1>
<hr>
<blockquote>
<p><em><strong>각각의 <code>@Annotation</code>은 이를 사용하거나, 사용하지 말아야 할 이유가 존재합니다.</strong></em></p>
</blockquote>
<blockquote>
</blockquote>
<ul>
<li><p><code>@Data</code>: 간단한 데이터 클래스 작성을 위한 종합적인 기능 제공</p>
<ul>
<li><code>@Getter</code>, <code>@Setter</code>, <code>@ToString</code> 포함</li>
<li><code>@EqualsAndHashCode</code>, <code>@RequiredArgsConstructor</code> 포함</li>
</ul>
</li>
<li><p><code>@Builder</code>: 빌더 패턴을 통해 객체 생성 지원</p>
</li>
<li><p><code>@NoArgsConstructor</code>: 매개변수가 없는 기본 생성자 자동 생성</p>
</li>
<li><p><code>@AllArgsConstructor</code>: 모든 필드를 매개변수로 갖는 생성자 자동 생성
변수명을 기준으로 매개변수를 필드에 자동으로 초기화</p>
</li>
<li><p><code>DTO</code>는 비교적 데이터 수정에 관대하므로, <code>@Data</code>를 사용해 중복 코드를 감소</p>
</li>
<li><p><code>생성자</code>는, 데이터베이스로부터 읽은 <code>Entity</code> 데이터를 <code>DTO</code>로 변환하는데 사용
<code>Entity</code> -&gt; <code>RequestDto</code> 클래스로 변환한다.</p>
</li>
</ul>
<br>

<h2 id="📖-postlistresponsedtojava">📖 PostListResponseDto.java</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.dto;

import com.companyname.projectname.post.model.Post;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostListResponseDto {
    private Long id;
    private String title;
    private LocalDateTime createdDate;
    private LocalDateTime updatedDate;

    // Custom Constructor, convert data from entity to dto
    public PostListResponseDto(Post entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.createdDate = entity.getCreatedDate();
        this.updatedDate = entity.getUpdatedDate();
    }
}
</code></pre>
<blockquote>
<p><em><strong>게시글의 목록을 조회할 때, <code>content(내용)</code>은 불필요하므로 필드에 기입하지 않습니다.</strong></em></p>
</blockquote>
<br>

<h2 id="📖-postdetailresponsedtojava">📖 PostDetailResponseDto.java</h2>
<hr>
<pre><code class="language-java">package com.companyname.projectname.post.dto;

import com.companyname.projectname.post.model.Post;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostDetailResponseDto {
    private Long id;
    private String title;
    private String content;
    private LocalDateTime createdDate;
    private LocalDateTime updatedDate;

    // Custom Constructor, convert data from entity to dto
    public PostDetailResponseDto(Post entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.createdDate = entity.getCreatedDate();
        this.updatedDate = entity.getUpdatedDate();
    }
}</code></pre>
<blockquote>
<p><em><strong>게시글을 상세 조회할 때, <code>content(내용)</code>이 필요하므로 필드에 기입합니다.</strong></em></p>
</blockquote>
<br>

<hr>
<strong>

<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.</code></pre></strong>]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] No space left on device]]></title>
            <link>https://velog.io/@sihoon_cho/Docker-No-space-left-on-device</link>
            <guid>https://velog.io/@sihoon_cho/Docker-No-space-left-on-device</guid>
            <pubDate>Tue, 02 Apr 2024 15:15:40 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-문제">📌 문제</h1>
<hr>
<h2 id="📖-문제-발생">📖 문제 발생</h2>
<p><code>Jenkins</code> <code>CI/CD</code> 빌드/배포 자동화 수행 중 오류가 발생</p>
<blockquote>
<p><strong>No space left on device</strong></p>
</blockquote>
<hr>
<h2 id="📖-문제-원인">📖 문제 원인</h2>
<ul>
<li><code>Jenkins</code> <code>CI/CD</code>가 반복적으로 수행되는 과정에서 <code>Build Cache</code>가 쌓이면서 문제 발생</li>
<li><code>$ docker system df</code>: 현재 사용중인 이미지, 컨테이너 및 볼륨이 얼마나 많은 공간을 사용하고 있는지 확인할 수 있는 명령어. 뒤에 <code>-v</code> (vervose)옵션을 추가하면 사용하지 않는 이미지와 컨테이너도 확인 가능</li>
</ul>
<pre><code class="language-shell">$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/root       311G  310G  284M 100% /
...</code></pre>
<pre><code class="language-shell">$ sudo docker system df -v</code></pre>
<pre><code class="language-shell">Images space usage:

REPOSITORY       TAG       IMAGE ID       CREATED        SIZE      SHARED SIZE   UNIQUE SIZE   CONTAINERS
server/backend   latest    aa4d75651096   4 hours ago    2.36GB    0B            2.356GB       1
server/flask     latest    2df4d4b0373f   9 hours ago    1.71GB    0B            1.714GB       1
server/sdwebui   latest    97b0cc844eb6   13 days ago    12.9GB    0B            12.9GB        1
server/jenkins   latest    53eb4ac17ccf   3 weeks ago    996MB     0B            996.1MB       1
redis            latest    170a1e90f843   2 months ago   138MB     0B            137.7MB       1</code></pre>
<pre><code class="language-shell">Containers space usage:

CONTAINER ID   IMAGE            COMMAND                  LOCAL VOLUMES   SIZE      CREATED       STATUS       NAMES
51fea23b7f65   server/backend   &quot;java -Dfile.encodin…&quot;   0               69.8MB    4 hours ago   Up 4 hours   server-backend
0bcf716ad266   server/flask     &quot;python -m app&quot;          0               75.9kB    9 hours ago   Up 9 hours   peaceful_poincare
ec6a6ba85069   redis:latest     &quot;docker-entrypoint.s…&quot;   1               970B      12 days ago   Up 12 days   server-redis
d45a7633117c   server/sdwebui   &quot;/bin/sh -c &#39;./webui…&quot;   0               1.52MB    13 days ago   Up 13 days   server-sdwebui
b4f0930a3620   server/jenkins   &quot;/usr/bin/tini -- /u…&quot;   0               2.68GB    2 weeks ago   Up 2 weeks   server-jenkins</code></pre>
<pre><code class="language-shell">Local Volumes space usage:

VOLUME NAME                                                        LINKS     SIZE
frontend                                                           0         986.1MB
99fbae4584bbc073085c13eb3521a2b8fc8e473e49d8e416bd0c2a0fad52d622   1         692B</code></pre>
<pre><code class="language-shell">Build cache usage: 239.1GB

CACHE ID       CACHE TYPE     SIZE      CREATED        LAST USED      USAGE     SHARED
tedxqgtdxyuo   regular        0B        3 weeks ago    3 weeks ago    1         true
o0ctbsb203zx   regular        0B        3 weeks ago    3 weeks ago    1         true
rj7x1gryphqf   regular        0B        3 weeks ago    3 weeks ago    1         true
lwkf9nf8n56i   regular        0B        3 weeks ago    3 weeks ago    1         true
9xgrulsw4agg   regular        0B        3 weeks ago    3 weeks ago    1         true
...
wnxw3nbkq9lq   source.local   1.08kB    13 days ago    4 hours ago    72        false
kk63u2877y75   source.local   1.43GB    13 days ago    4 hours ago    72        false
u961phqnm9hk   source.local   0B        13 days ago    4 hours ago    72        false
yi0bk5rb8196   regular        1.43GB    4 hours ago    4 hours ago    1         false
qx24ltqc2i91   regular        374MB     4 hours ago    4 hours ago    1         false</code></pre>
<hr>
<h1 id="📌-해결">📌 해결</h1>
<hr>
<h2 id="📖-해결-방법">📖 해결 방법</h2>
<p><code>$ docker {option} prune</code>: 사용하지 않는 컨테이너 이미지를 제거하는 명령어.</p>
<ul>
<li><code>docker volume prune</code>: 미사용 볼륨 제거</li>
<li><code>docker container prune</code>: 미사용 컨테이너 제거</li>
<li><code>docker image prune</code>: 미사용 이미지 제거</li>
<li><code>docker system prune</code>: 미사용 중인 이미지, 컨테이너, 볼륨 모두 제거</li>
</ul>
<pre><code class="language-shell">$ sudo docker system prune
WARNING! This will remove:
  - all stopped containers
  - all networks not used by at least one container
  - all dangling images
  - unused build cache

Are you sure you want to continue? [y/N] y
Deleted build cache objects:
srcnm8ftb1v79f6ettaf2bb6z
w8680tyap8d997hqucw9q6r44
pqtcfdmmueoyvjn7k3d5n0kat
i9opfym5x8wqm66huz0hxqu8e
v36akpatuyaoa7xgwb5sq89zi
...
972yylz5mtkcx7jpk2aknp1go
ocgyuijoqbqozrtiryt5xsuk6
i44z9mydueux37f9axhgecs1p
kmxd7dz6hu9izv9ww8egsdfzf
rp8d0tqyi6f8g0fftmjaqrb2a

Total reclaimed space: 239.1GB</code></pre>
<hr>
<h2 id="📖-결과-확인">📖 결과 확인</h2>
<pre><code class="language-shell">$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/root       311G   76G  235G  25% /
...</code></pre>
<pre><code class="language-shell">$ sudo docker system df -v
Images space usage:

REPOSITORY       TAG       IMAGE ID       CREATED        SIZE      SHARED SIZE   UNIQUE SIZE   CONTAINERS
server/backend   latest    aa4d75651096   4 hours ago    2.36GB    0B            2.356GB       1
server/flask     latest    2df4d4b0373f   9 hours ago    1.71GB    0B            1.714GB       1
server/sdwebui   latest    97b0cc844eb6   13 days ago    12.9GB    0B            12.9GB        1
server/jenkins   latest    53eb4ac17ccf   3 weeks ago    996MB     0B            996.1MB       1
redis            latest    170a1e90f843   2 months ago   138MB     0B            137.7MB       1</code></pre>
<pre><code class="language-shell">Containers space usage:

CONTAINER ID   IMAGE            COMMAND                  LOCAL VOLUMES   SIZE      CREATED       STATUS       NAMES
51fea23b7f65   server/backend   &quot;java -Dfile.encodin…&quot;   0               69.8MB    4 hours ago   Up 4 hours   server-backend
0bcf716ad266   server/flask     &quot;python -m app&quot;          0               75.9kB    9 hours ago   Up 9 hours   peaceful_poincare
ec6a6ba85069   redis:latest     &quot;docker-entrypoint.s…&quot;   1               970B      12 days ago   Up 12 days   server-redis
d45a7633117c   server/sdwebui   &quot;/bin/sh -c &#39;./webui…&quot;   0               1.52MB    13 days ago   Up 13 days   server-sdwebui
b4f0930a3620   server/jenkins   &quot;/usr/bin/tini -- /u…&quot;   0               2.68GB    2 weeks ago   Up 2 weeks   server-jenkins</code></pre>
<pre><code class="language-shell">Local Volumes space usage:

VOLUME NAME                                                        LINKS     SIZE
frontend                                                           0         986.1MB
99fbae4584bbc073085c13eb3521a2b8fc8e473e49d8e416bd0c2a0fad52d622   1         692B</code></pre>
<pre><code class="language-shell">Build cache usage: 0B

CACHE ID       CACHE TYPE   SIZE      CREATED       LAST USED     USAGE     SHARED
tedxqgtdxyuo   regular      0B        3 weeks ago   3 weeks ago   1         true
o0ctbsb203zx   regular      0B        3 weeks ago   3 weeks ago   1         true
rj7x1gryphqf   regular      0B        3 weeks ago   3 weeks ago   1         true
lwkf9nf8n56i   regular      0B        3 weeks ago   3 weeks ago   1         true
9xgrulsw4agg   regular      0B        3 weeks ago   3 weeks ago   1         true
...
nasnj8zhsxh2   regular      0B        8 days ago    9 hours ago   26        true
jjsnpxdic89f   regular      173MB     9 hours ago   9 hours ago   1         true
xu7sjqzi7lbr   regular      0B        5 days ago    4 hours ago   50        true
u3han9rdyypr   regular      1.53GB    4 hours ago   4 hours ago   1         true
ua8dcwafo6s4   regular      172MB     4 hours ago   4 hours ago   1         true</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS/EC2] AWS EC2 Jenkins CI/CD]]></title>
            <link>https://velog.io/@sihoon_cho/AWSEC2-AWS-EC2-Jenkins-CICD</link>
            <guid>https://velog.io/@sihoon_cho/AWSEC2-AWS-EC2-Jenkins-CICD</guid>
            <pubDate>Thu, 14 Mar 2024 08:02:56 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 기반으로 작성되었습니다.
실습 위주의 이해를 목표로 하기 때문에 다소 과장이 많고 생략된 부분이 많을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 대해 유의하시기 바랍니다.

또한, 본 시리즈는 ChatGPT의 도움을 받아 작성되었습니다.
수 차례의 질문을 통해 도출된 여러가지 다양한 방식의 코드를 종합하여
작성자의 이해와 경험을 바탕으로 가장 정석으로 생각되는 코드를 재정립하였습니다.</code></pre><hr>
<h1 id="📌-jenkins-plugin-설치">📌 Jenkins Plugin 설치</h1>
<ul>
<li><code>NodeJS</code>, <code>GitLab</code> 플러그인 설치</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/dc536ab0-777d-46ce-8081-c8aad3344d66/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/d0db4757-4d41-476e-8b7f-fb756138bb34/image.png" alt=""></p>
<hr>
<h1 id="📌-jenkins-gitlab-설정">📌 Jenkins GitLab 설정</h1>
<hr>
<h2 id="📖-gitlab-accesstoken-발급">📖 GitLab AccessToken 발급</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/a52f0643-e4d0-4075-b05d-d02fa34db54e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/66d64012-a1fe-4bec-9077-66e4e3e98875/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/59b3ac02-50a5-4de9-9242-84b5d4e2dcfb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/6cd6b2b8-2c79-4bba-b709-c4ef5514fcd1/image.png" alt=""></p>
<hr>
<h2 id="📖-jenkins-credential-설정">📖 Jenkins Credential 설정</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/39c90f62-ce93-4014-8b6e-93cea9f2810d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/7d78e5dd-5a77-4a94-9746-88dd9f222dee/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/dc836fb5-7056-414c-b874-00ecf6d3f43c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/8241d8a7-d0e0-40a4-916a-95eda477a637/image.png" alt=""></p>
<hr>
<h2 id="📖-jenkins-system-설정">📖 Jenkins System 설정</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/262761c4-2058-4787-adac-614c44771ef2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/b39ae886-1ec9-4283-8d4a-6a79f137c4f9/image.png" alt=""></p>
<hr>
<h2 id="📖-jenkins-tools-nodejs-설정">📖 Jenkins Tools NodeJS 설정</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/69c5a7ef-3c64-4add-a2e8-cbb13a655605/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/d2fae45b-9c9e-4cd6-a250-7bae7f8869e4/image.png" alt=""></p>
<hr>
<h1 id="📌-react-dockerfile-작성">📌 React Dockerfile 작성</h1>
<ul>
<li><code>/frontend</code> 에 <code>Dockerfile</code> 작성</li>
</ul>
<pre><code class="language-bash"># Node.js 공식 이미지 사용. 버전 20.11.1, 경량화된 Alpine Linux 기반
FROM node:20.11.1-alpine

# 작업 디렉토리 설정. 컨테이너 내 앱의 기본 경로
WORKDIR /home/app

# 현재 디렉토리의 package.json과 package-lock.json 파일이 존재한다면
# 컨테이너의 작업 디렉토리로 package.json과 package-lock.json 복사
COPY package*.json ./

# package.json에 명시된 애플리케이션 의존성 설치
# package-lock.json이 있을 경우 더 빠르게 설치 가능
RUN npm install

# 현재 디렉토리의 모든 파일을 컨테이너의 작업 디렉토리로 복사
COPY . .

# React 애플리케이션 빌드
RUN npm run build</code></pre>
<hr>
<h1 id="📌-springboot-dockerfile-작성">📌 SpringBoot Dockerfile 작성</h1>
<ul>
<li><code>/backend</code> 에 <code>Dockerfile</code> 작성</li>
</ul>
<pre><code class="language-bash"># OpenJDK 17을 포함하는 경량화된 Alpine Linux 베이스 이미지 사용
FROM openjdk:17-jdk-alpine

# 컨테이너 내부의 작업 디렉토리를 /home/app로 설정
WORKDIR /home/app

# 호스트 시스템의 SpringBoot 애플리케이션 JAR 파일을 컨테이너 내부 작업 디렉토리로 복사
COPY build/libs/*.jar app.jar

# 컨테이너가 시작될 때 실행될 명령어 정의, 작업 디렉토리 /home/app/app.jar 파일 실행
ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;./app.jar&quot;]

# 컨테이너의 8080 포트를 외부로 노출
EXPOSE 8080</code></pre>
<hr>
<h1 id="📌-jenkins-pipeline-설정">📌 Jenkins Pipeline 설정</h1>
<hr>
<h2 id="📖-jenkins-credential-설정-1">📖 Jenkins Credential 설정</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/39c90f62-ce93-4014-8b6e-93cea9f2810d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/7d78e5dd-5a77-4a94-9746-88dd9f222dee/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/72ebcf01-944c-43f6-929f-2f907c0ebae8/image.png" alt=""></p>
<hr>
<h2 id="📖-pipeline-생성">📖 Pipeline 생성</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/763c3fd3-9fb1-4071-a419-58fcc5519efc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/2ef3bf58-c4c0-47f9-9c3d-2270e2f7616e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/80cb6f8b-da4e-47f9-b3e6-f34bd2530434/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>초록색 박스로 표시된 <code>GitLab webhook URL</code>을 잘 기록해둔다.
초록색 박스로 표시된 고급 탭에서 <code>Generate</code>한 <code>Secret token</code>을 잘 기록해둔다.</p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/49f59781-1680-480b-b7a1-6ed50cd8e933/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/f7bb8e86-bf51-4aef-84f7-56f764a6ee3c/image.png" alt=""></p>
<pre><code class="language-bash">git branch: &#39;master&#39;, credentialsId: &#39;YOUR_CREDENTIAL&#39;, url: &#39;https://&lt;Your Git Repository URL&gt;&#39;</code></pre>
<pre><code class="language-bash">pipeline {
    agent any

    tools {
        nodejs &#39;nodejs-20.11.1&#39;
    }

    // 필요한 변수 설정
    environment {
        PROJECT_DIR = &#39;your_project_directory_name&#39;
        DOCKER_REGISTRY = &#39;your_docker_registry_url&#39;

        BACKEND_IMAGE_NAME = &#39;server/backend&#39;
        FRONTEND_IMAGE_NAME = &#39;server/frontend&#39;

        BACKEND_CONTAINER_NAME = &#39;server-backend&#39;
        FRONTEND_CONTAINER_NAME = &#39;server-frontend&#39;
    }


    stages {
        stage(&#39;Checkout&#39;) {
            steps {
                echo &#39;Starting Repository Checkout&#39;

                git url: &#39;https://&lt;Your Git Repository URL&gt;&#39;,
                credentialsId: &#39;YOUR_CREDENTIAL&#39;, branch: &#39;master&#39;

                echo &#39;Repository Checkout Completed&#39;
            }
        }

        stage(&#39;Build Frontend&#39;) {
            steps {
                echo &#39;Starting Frontend Build Process&#39;

                dir(&#39;frontend&#39;) {
                    sh &#39;docker ps -aqf &quot;name=${FRONTEND_CONTAINER_NAME}&quot; &amp;&amp; docker stop ${FRONTEND_CONTAINER_NAME} &amp;&amp; docker rm ${FRONTEND_CONTAINER_NAME}&#39;
                    sh &#39;docker images -q ${FRONTEND_IMAGE_NAME} &amp;&amp; docker rmi ${FRONTEND_IMAGE_NAME}&#39;
                    sh &#39;docker build -t ${FRONTEND_IMAGE_NAME} .&#39;
                }

                echo &#39;Frontend Build Completed Successfully&#39;
            }
            post {
                success {
                    script {
                        echo &#39;Build Frontend Success&#39;
                    }
                }
                failure {
                    script {
                        echo &#39;Build Frontend Failed&#39;
                    }
                }
            }
        }

        stage(&#39;Test Frontend&#39;) {
            steps {
                echo &#39;Starting Frontend Tests&#39;

                dir(&#39;frontend&#39;) {
                    sh &#39;npm test&#39;
                }

                echo &#39;Frontend Tests Completed&#39;
            }
            post {
                success {
                    script {
                        echo &#39;Test Frontend Success&#39;
                    }
                }
                failure {
                    script {
                        echo &#39;Test Frontend Failed&#39;
                    }
                }
            }
        }

        stage(&#39;Deploy Frontend&#39;) {
            steps {
                echo &#39;Deploying Frontend&#39;

                sh &quot;docker run -d -p 3000:3000 --name ${FRONTEND_CONTAINER_NAME} ${FRONTEND_IMAGE_NAME}&quot;

                echo &#39;Frontend Deployed Successfully&#39;
            }
            post {
                success {
                    script {
                        echo &#39;Deploy Frontend Success&#39;
                    }
                }
                failure {
                    script {
                        echo &#39;Deploy Frontend Failed&#39;
                    }
                }
            }
        }

        stage(&#39;Build Backend&#39;) {
            steps {
                echo &#39;Starting Backend Build Process&#39;

                dir(&#39;backend&#39;) {
                    sh &#39;chmod +x ./gradlew&#39;
                    sh &#39;./gradlew clean build&#39;

                    sh &#39;docker ps -aqf &quot;name=${BACKEND_CONTAINER_NAME}&quot; &amp;&amp; docker stop ${BACKEND_CONTAINER_NAME} &amp;&amp; docker rm ${BACKEND_CONTAINER_NAME}&#39;
                    sh &#39;docker images -q ${BACKEND_IMAGE_NAME} &amp;&amp; docker rmi ${BACKEND_IMAGE_NAME}&#39;
                    sh &#39;docker build -t ${BACKEND_IMAGE_NAME} .&#39;
                }

                echo &#39;Backend Build Completed Successfully&#39;
            }
            post {
                success {
                    script {
                        echo &#39;Build Backend Success&#39;
                    }
                }
                failure {
                    script {
                        echo &#39;Build Backend Failed&#39;
                    }
                }
            }
        }

        stage(&#39;Test Backend&#39;) {
            steps {
                echo &#39;Starting Backend Tests&#39;

                dir(&#39;backend&#39;) {
                    sh &#39;./gradlew test&#39;
                }

                echo &#39;Backend Tests Completed&#39;
            }
            post {
                success {
                    script {
                        echo &#39;Test Backend Success&#39;
                    }
                }
                failure {
                    script {
                        echo &#39;Test Backend Failed&#39;
                    }
                }
            }
        }

        stage(&#39;Deploy Backend&#39;) {
            steps {
                echo &#39;Deploying Backend&#39;

                sh &quot;docker run -d -p 8080:8080 --name ${BACKEND_CONTAINER_NAME} ${BACKEND_IMAGE_NAME}&quot;

                echo &#39;Backend Deployed Successfully&#39;
            }
            post {
                success {
                    script {
                        echo &#39;Deploy Backend Success&#39;
                    }
                }
                failure {
                    script {
                        echo &#39;Deploy Backend Failed&#39;
                    }
                }
            }
        }
    }

    post {
        always {
            echo &#39;Pipeline Execution Complete.&#39;
        }
        success {
            echo &#39;Pipeline Execution Success.&#39;
            script {
                echo &#39;빌드/배포 Success&#39;
            }
        }
        failure {
            echo &#39;Pipeline Execution Failed.&#39;
            script {
                echo &#39;빌드/배포 Failed&#39;
            }
        }
    }
}</code></pre>
<hr>
<h2 id="📖-gitlab-webhook-추가">📖 GitLab Webhook 추가</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/8081c852-d886-4e1b-9a6a-e5b9f57c8326/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/3520317f-b657-4ec1-bcf4-95a803626728/image.png" alt=""></p>
<blockquote>
</blockquote>
<ul>
<li><code>URL</code>: 위에서 기록해둔 <code>GitLab webhook URL</code></li>
<li><code>Secret token</code>: 위에서 기록해둔 <code>Secret token</code></li>
</ul>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/6e24a897-b474-4247-8ff5-ea0be2d59856/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/0d9b60fe-8cf6-45c2-a4ae-c366e2bc3a06/image.png" alt=""></p>
<hr>
<h1 id="📌-mattermost-webhook-설정">📌 Mattermost Webhook 설정</h1>
<hr>
<h2 id="📖-mattermost-webhook-추가">📖 Mattermost Webhook 추가</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/af0ef0e5-29fe-4dc1-9f19-1e5a2db9d39e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/18ea5012-4cea-4b4f-9cd3-3e3473ec99a9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/bb0984c6-0cf6-420a-8f2d-29dbaceb4d3c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/eda49670-f9ff-4da1-b61f-3192fbd2113c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/9ed2b852-4436-4155-9e39-29f4703ac5b4/image.png" alt=""></p>
<hr>
<h2 id="📖-jenkins-mattermost-plugin-설치">📖 Jenkins Mattermost Plugin 설치</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/dc536ab0-777d-46ce-8081-c8aad3344d66/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/d3ad717c-3ee6-4dee-9c8e-ef37266d9adc/image.png" alt=""></p>
<hr>
<h2 id="📖-jenkins-system-설정-1">📖 Jenkins System 설정</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/c2714cc8-47ae-45dc-a1a2-dd445fad3b4c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/262761c4-2058-4787-adac-614c44771ef2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/de82fc9b-e6d6-4d6e-b454-11676d307863/image.png" alt=""></p>
<blockquote>
</blockquote>
<ul>
<li><code>Endpoint</code>: Incoming Webhook URL</li>
<li><code>Channel</code>: Incoming Webhook을 추가할 때 선택했던 채널 이름 (초록색 박스 안의 주소)</li>
<li><code>Build Server URL</code>: Jenkins URL (자동으로 입력되어 있을 것이다.)</li>
</ul>
<hr>
<h2 id="📖-공통-함수-정의">📖 공통 함수 정의</h2>
<pre><code class="language-bash">// 공통 함수 정의
def sendMattermostNotification(String stage, String status) {
    script {
        def AUTHOR_ID = sh(script: &quot;git show -s --pretty=%an&quot;, returnStdout: true).trim()
        def AUTHOR_NAME = sh(script: &quot;git show -s --pretty=%ae&quot;, returnStdout: true).trim()

        def color = (status == &#39;Success&#39;) ? &#39;good&#39; : &#39;danger&#39;
        def message = &quot;${stage} ${status}: ${env.JOB_NAME} #${env.BUILD_NUMBER} by ${AUTHOR_ID}(${AUTHOR_NAME})\n(&lt;${env.BUILD_URL}|Details&gt;)&quot;
        def endpoint = &#39;https://your_notification_service_endpoint&#39;
        def channel = &#39;your_notification_service_channel&#39;

        mattermostSend (
            color: color,
            message: message,
            endpoint: endpoint,
            channel: channel,
        )
    }
}

pipeline {
    agent any

    ...
}</code></pre>
<hr>
<h2 id="📖-pipeline-적용">📖 Pipeline 적용</h2>
<pre><code class="language-bash">// 공통 함수 정의
def sendMattermostNotification(String stage, String status) {
    script {
        def AUTHOR_ID = sh(script: &quot;git show -s --pretty=%an&quot;, returnStdout: true).trim()
        def AUTHOR_NAME = sh(script: &quot;git show -s --pretty=%ae&quot;, returnStdout: true).trim()

        def color = (status == &#39;Success&#39;) ? &#39;good&#39; : &#39;danger&#39;
        def message = &quot;${stage} ${status}: ${env.JOB_NAME} #${env.BUILD_NUMBER} by ${AUTHOR_ID}(${AUTHOR_NAME})\n(&lt;${env.BUILD_URL}|Details&gt;)&quot;
        def endpoint = &#39;https://your_notification_service_endpoint&#39;
        def channel = &#39;your_notification_service_channel&#39;

        mattermostSend (
            color: color,
            message: message,
            endpoint: endpoint,
            channel: channel,
        )
    }
}

pipeline {
    agent any

    tools {
        nodejs &#39;nodejs-20.11.1&#39;
    }

    // 필요한 변수 설정
    environment {
        PROJECT_DIR = &#39;your_project_directory_name&#39;
        DOCKER_REGISTRY = &#39;your_docker_registry_url&#39;

        BACKEND_IMAGE_NAME = &#39;server/backend&#39;
        FRONTEND_IMAGE_NAME = &#39;server/frontend&#39;

        BACKEND_CONTAINER_NAME = &#39;server-backend&#39;
        FRONTEND_CONTAINER_NAME = &#39;server-frontend&#39;
    }


    stages {
        stage(&#39;Checkout&#39;) {
            steps {
                echo &#39;Starting Repository Checkout&#39;

                git url: &#39;https://&lt;Your Git Repository URL&gt;&#39;,
                credentialsId: &#39;YOUR_CREDENTIAL&#39;, branch: &#39;master&#39;

                echo &#39;Repository Checkout Completed&#39;
            }
        }

        stage(&#39;Build Frontend&#39;) {
            steps {
                echo &#39;Starting Frontend Build Process&#39;

                dir(&#39;frontend&#39;) {
                    sh &#39;docker ps -aqf &quot;name=${FRONTEND_CONTAINER_NAME}&quot; &amp;&amp; docker stop ${FRONTEND_CONTAINER_NAME} &amp;&amp; docker rm ${FRONTEND_CONTAINER_NAME}&#39;
                    sh &#39;docker images -q ${FRONTEND_IMAGE_NAME} &amp;&amp; docker rmi ${FRONTEND_IMAGE_NAME}&#39;
                    sh &#39;docker build -t ${FRONTEND_IMAGE_NAME} .&#39;
                }

                echo &#39;Frontend Build Completed Successfully&#39;
            }
            post {
                success {
                    script {
                        sendMattermostNotification(&#39;Build Frontend&#39;, &#39;Success&#39;)
                    }
                }
                failure {
                    script {
                        sendMattermostNotification(&#39;Build Frontend&#39;, &#39;Failed&#39;)
                    }
                }
            }
        }

        stage(&#39;Test Frontend&#39;) {
            steps {
                echo &#39;Starting Frontend Tests&#39;

                dir(&#39;frontend&#39;) {
                    sh &#39;npm test&#39;
                }

                echo &#39;Frontend Tests Completed&#39;
            }
            post {
                success {
                    script {
                        sendMattermostNotification(&#39;Test Frontend&#39;, &#39;Success&#39;)
                    }
                }
                failure {
                    script {
                        sendMattermostNotification(&#39;Test Frontend&#39;, &#39;Failed&#39;)
                    }
                }
            }
        }

        stage(&#39;Deploy Frontend&#39;) {
            steps {
                echo &#39;Deploying Frontend&#39;

                sh &quot;docker run -d -p 3000:3000 --name ${FRONTEND_CONTAINER_NAME} ${FRONTEND_IMAGE_NAME}&quot;

                echo &#39;Frontend Deployed Successfully&#39;
            }
            post {
                success {
                    script {
                        sendMattermostNotification(&#39;Deploy Frontend&#39;, &#39;Success&#39;)
                    }
                }
                failure {
                    script {
                        sendMattermostNotification(&#39;Deploy Frontend&#39;, &#39;Failed&#39;)
                    }
                }
            }
        }

        stage(&#39;Build Backend&#39;) {
            steps {
                echo &#39;Starting Backend Build Process&#39;

                dir(&#39;backend&#39;) {
                    sh &#39;chmod +x ./gradlew&#39;
                    sh &#39;./gradlew clean build&#39;

                    sh &#39;docker ps -aqf &quot;name=${BACKEND_CONTAINER_NAME}&quot; &amp;&amp; docker stop ${BACKEND_CONTAINER_NAME} &amp;&amp; docker rm ${BACKEND_CONTAINER_NAME}&#39;
                    sh &#39;docker images -q ${BACKEND_IMAGE_NAME} &amp;&amp; docker rmi ${BACKEND_IMAGE_NAME}&#39;
                    sh &#39;docker build -t ${BACKEND_IMAGE_NAME} .&#39;
                }

                echo &#39;Backend Build Completed Successfully&#39;
            }
            post {
                success {
                    script {
                        sendMattermostNotification(&#39;Build Backend&#39;, &#39;Success&#39;)
                    }
                }
                failure {
                    script {
                        sendMattermostNotification(&#39;Build Backend&#39;, &#39;Failed&#39;)
                    }
                }
            }
        }

        stage(&#39;Test Backend&#39;) {
            steps {
                echo &#39;Starting Backend Tests&#39;

                dir(&#39;backend&#39;) {
                    sh &#39;./gradlew test&#39;
                }

                echo &#39;Backend Tests Completed&#39;
            }
            post {
                success {
                    script {
                        sendMattermostNotification(&#39;Test Backend&#39;, &#39;Success&#39;)
                    }
                }
                failure {
                    script {
                        sendMattermostNotification(&#39;Test Backend&#39;, &#39;Failed&#39;)
                    }
                }
            }
        }

        stage(&#39;Deploy Backend&#39;) {
            steps {
                echo &#39;Deploying Backend&#39;

                sh &quot;docker run -d -p 8080:8080 --name ${BACKEND_CONTAINER_NAME} ${BACKEND_IMAGE_NAME}&quot;

                echo &#39;Backend Deployed Successfully&#39;
            }
            post {
                success {
                    script {
                        sendMattermostNotification(&#39;Deploy Backend&#39;, &#39;Success&#39;)
                    }
                }
                failure {
                    script {
                        sendMattermostNotification(&#39;Deploy Backend&#39;, &#39;Failed&#39;)
                    }
                }
            }
        }
    }

    post {
        always {
            echo &#39;Pipeline Execution Complete.&#39;
        }
        success {
            echo &#39;Pipeline Execution Success.&#39;
            script {
                sendMattermostNotification(&#39;빌드/배포&#39;, &#39;Success&#39;)
            }
        }
        failure {
            echo &#39;Pipeline Execution Failed.&#39;
            script {
                sendMattermostNotification(&#39;빌드/배포&#39;, &#39;Failed&#39;)
            }
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS/EC2] AWS EC2 MySQL 설치]]></title>
            <link>https://velog.io/@sihoon_cho/AWSEC2-AWS-EC2-MySQL</link>
            <guid>https://velog.io/@sihoon_cho/AWSEC2-AWS-EC2-MySQL</guid>
            <pubDate>Thu, 14 Mar 2024 01:57:26 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 기반으로 작성되었습니다.
실습 위주의 이해를 목표로 하기 때문에 다소 과장이 많고 생략된 부분이 많을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 대해 유의하시기 바랍니다.

또한, 본 시리즈는 ChatGPT의 도움을 받아 작성되었습니다.
수 차례의 질문을 통해 도출된 여러가지 다양한 방식의 코드를 종합하여
작성자의 이해와 경험을 바탕으로 가장 정석으로 생각되는 코드를 재정립하였습니다.</code></pre><hr>
<h1 id="📌-mysql-설치">📌 MySQL 설치</h1>
<ul>
<li><code>root</code> 비밀번호 변경</li>
<li><code>새 계정</code> 추가 및 외부접속 허용</li>
<li><code>Shell Script</code>를 사용하여 자동화</li>
</ul>
<hr>
<h2 id="📖-shell-script-작성">📖 Shell Script 작성</h2>
<p><code>install_mysql.sh</code></p>
<pre><code class="language-shell">#!/bin/bash

# 변수명 설정
ROOT_PASSWORD=&#39;your_root_password&#39;
NEW_USERNAME=&#39;your_new_mysql_username&#39;
NEW_PASSWORD=&#39;your_new_mysql_password&#39;

# 방화벽 설정
if sudo ufw status | grep -qw inactive; then
    echo &quot;방화벽이 비활성화되어 있습니다. 방화벽을 활성화합니다.&quot;
    sudo ufw enable
fi
sudo ufw allow 3306

# MySQL 설치
echo &quot;MySQL 설치를 시작합니다...&quot;
sudo apt-get update
sudo apt-get install -y mysql-server

# MySQL 서비스 시작
sudo systemctl start mysql
sudo systemctl enable mysql

# 루트 비밀번호 설정 및 보안 설치 실행
sudo mysql -e &quot;ALTER USER &#39;root&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;${ROOT_PASSWORD}&#39;;&quot;
sudo mysql -e &quot;FLUSH PRIVILEGES;&quot;

# 사용자 추가 스크립트
sudo mysql -e &quot;CREATE USER &#39;${NEW_USERNAME}&#39;@&#39;%&#39; IDENTIFIED BY &#39;${NEW_PASSWORD}&#39;;&quot;
sudo mysql -e &quot;GRANT ALL PRIVILEGES ON *.* TO &#39;${NEW_USERNAME}&#39;@&#39;%&#39; WITH GRANT OPTION;&quot;
sudo mysql -e &quot;FLUSH PRIVILEGES;&quot;

# MySQL 설정 파일에서 bind-address 값을 0.0.0.0으로 변경하여 어느 주소에서든 접근 가능하도록 설정
sudo sed -i &#39;/bind-address/s/^#//g&#39; /etc/mysql/mysql.conf.d/mysqld.cnf
sudo sed -i &#39;s/127.0.0.1/0.0.0.0/g&#39; /etc/mysql/mysql.conf.d/mysqld.cnf

# MySQL 서비스 재시작
sudo systemctl restart mysql

echo &quot;MySQL 설치 및 사용자 추가가 완료되었습니다.&quot;</code></pre>
<ul>
<li><code>%</code> 권한: 모든 IP 접근 허용, MySQL Workbench 등 외부접속이 필요한 경우 사용</li>
<li><code>localhost</code> 권한: <code>localhost</code> IP 접근 허용,
<code>root</code> 계정 등 외부접속으로부터 보안상 보호해야할 경우 사용</li>
</ul>
<hr>
<h2 id="📖-shell-script-실행">📖 Shell Script 실행</h2>
<pre><code class="language-shell">$ sudo chmod +x install_mysql.sh
$ ./install_mysql.sh</code></pre>
<hr>
<h1 id="📌-mysql-삭제">📌 MySQL 삭제</h1>
<ul>
<li><code>Shell Script</code>를 사용하여 자동화</li>
<li>MySQL 클린 삭제</li>
</ul>
<hr>
<h2 id="📖-shell-script-작성-1">📖 Shell Script 작성</h2>
<p><code>uninstall_mysql.sh</code></p>
<pre><code class="language-shell">#!/bin/bash

# 사용자에게 MySQL 삭제 확인 메시지 표시
echo &quot;MySQL 클린 삭제를 시작합니다. 모든 MySQL 데이터가 제거됩니다.&quot;
read -rp &quot;정말로 MySQL을 클린 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. (y/n): &quot; confirm

if [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]]; then
    echo &quot;MySQL 클린 삭제를 시작합니다...&quot;

    # MySQL 서비스 중지
    sudo systemctl stop mysql

    # MySQL 패키지 및 관련 패키지 제거
    sudo apt-get remove --purge -y mysql-server mysql-client mysql-common mysql-server-core-* mysql-c&gt;    sudo apt-get autoremove -y
    sudo apt-get autoclean -y

    # MySQL 설정 파일 및 데이터베이스 디렉토리 삭제
    sudo rm -rf /etc/mysql /var/lib/mysql

    # MySQL 로그 파일 삭제
    sudo rm -rf /var/log/mysql

    # MySQL 사용자 및 그룹 삭제 (선택적)
    sudo deluser mysql
    sudo delgroup mysql

    echo &quot;MySQL이 시스템에서 완전히 제거되었습니다.&quot;
else
    echo &quot;MySQL 클린 삭제가 취소되었습니다.&quot;</code></pre>
<hr>
<h2 id="📖-shell-script-실행-1">📖 Shell Script 실행</h2>
<pre><code class="language-shell">$ sudo chmod +x uninstall_mysql.sh
$ ./uninstall_mysql.sh</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS/EC2] AWS EC2 Docker Jenkins 설치]]></title>
            <link>https://velog.io/@sihoon_cho/AWSEC2-AWS-EC2-Docker-Jenkins-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@sihoon_cho/AWSEC2-AWS-EC2-Docker-Jenkins-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Tue, 12 Mar 2024 17:22:31 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 기반으로 작성되었습니다.
실습 위주의 이해를 목표로 하기 때문에 다소 과장이 많고 생략된 부분이 많을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 대해 유의하시기 바랍니다.

또한, 본 시리즈는 ChatGPT의 도움을 받아 작성되었습니다.
수 차례의 질문을 통해 도출된 여러가지 다양한 방식의 코드를 종합하여
작성자의 이해와 경험을 바탕으로 가장 정석으로 생각되는 코드를 재정립하였습니다.</code></pre><hr>
<h1 id="📌-docker-jenkins-수동설치">📌 Docker Jenkins 수동설치</h1>
<ul>
<li>개발자가 직접 명령어를 실행하며 설치하는 방법</li>
</ul>
<hr>
<h2 id="📖-docker-jenkins-image-pull">📖 Docker Jenkins Image Pull</h2>
<ul>
<li>DockerHub에 존재하는 이미지를 pull</li>
<li>아래 세 개 중 어느 걸 사용해도 무방</li>
<li>실행하지 않아도 상관없음<pre><code class="language-bash">$ sudo docker pull jenkins/jenkins:lts
$ sudo docker pull jenkins/jenkins:jdk17
$ sudo docker pull jenkins/jenkins:lts-jdk17 (권장)</code></pre>
</li>
</ul>
<hr>
<h2 id="📖-docker-jenkins-container-실행">📖 Docker Jenkins Container 실행</h2>
<pre><code class="language-bash">$ sudo docker run -d -u root \
    -p 8081:8080 -p 50000:50000 \
    -v /var/jenkins_home:/var/jenkins_home \
    -v /var/run/docker.sock:/var/run/docker.sock \
    --name server-jenkins jenkins/jenkins:lts-jdk17
    # --name server-jenkins jenkins/jenkins:lts
    # --name server-jenkins jenkins/jenkins:jdk17</code></pre>
<pre><code>-u 실행할 계정 지정
-d detached mode, 흔히 말하는 백그라운드 모드
-p 호스트와 컨테이너의 포트를 연결 (포워딩)
-v 호스트와 컨테이너의 디렉토리를 연결 (마운트)
--name 컨테이너 이름 설정

맨 마지막 jenkins/jenkins:lts-jdk17 은 위에서 pull한 이미지 이름
만약, 위에서 pull한 이미지가 없을 경우, DockerHub 에서 자동으로 pull</code></pre><hr>
<h2 id="📖-docker-jenkins-docker-cli-설치">📖 Docker Jenkins Docker CLI 설치</h2>
<ul>
<li><code>root</code> 계정은 <code>sudo</code> 명령어 없음</li>
<li>2024 Docker 공식문서 기준 설치방법 채용</li>
<li>Docker Jenkins Container는 debian 계열 linux</li>
<li><a href="https://docs.docker.com/engine/install/debian/">[Docker 공식문서]</a> <a href="https://docs.docker.com/engine/install/debian/">https://docs.docker.com/engine/install/debian/</a></li>
<li><a href="https://hub.docker.com/r/jenkins/jenkins/tags">[DockerHub 공식문서]</a> <a href="https://docs.docker.com/engine/install/debian/">https://docs.docker.com/engine/install/debian/</a></li>
</ul>
<pre><code class="language-bash"># Docker Container 접속
$ sudo docker exec -it server-jenkins /bin/bash</code></pre>
<pre><code class="language-bash"># Add Docker&#39;s official GPG key:
$ apt-get update
$ apt-get install ca-certificates curl
$ install -m 0755 -d /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
$ chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
$ echo &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
    $(. /etc/os-release &amp;&amp; echo &quot;$VERSION_CODENAME&quot;) stable&quot; | tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</code></pre>
<pre><code class="language-bash"># Docker CLI 설치
$ apt-get update
$ apt-get install -y docker-ce docker-ce-cli containerd.io</code></pre>
<hr>
<h1 id="📌-docker-jenkins-자동설치">📌 Docker Jenkins 자동설치</h1>
<ul>
<li><code>Dockerfile</code>과 <code>Shell Script</code>로 모든 설치과정을 자동화</li>
<li><code>Dockerfile</code>과 <code>Shell Script</code>가 같은 경로에 위치해야 함</li>
</ul>
<pre><code class="language-bash">$ mkdir jenkins
$ cd jenkins

$ sudo nano Dockerfile
$ sudo nano install_jenkins.sh

$ sudo chmod +x install_jenkins.sh
$ ./install_jenkins.sh</code></pre>
<hr>
<h2 id="📖-dockerfile-작성">📖 Dockerfile 작성</h2>
<ul>
<li><code>root</code> 계정은 <code>sudo</code> 명령어 없음</li>
<li>2024 Docker 공식문서 기준 설치방법 채용</li>
<li>Docker Jenkins Container는 debian 계열 linux</li>
<li><a href="https://docs.docker.com/engine/install/debian/">[Docker 공식문서]</a> <a href="https://docs.docker.com/engine/install/debian/">https://docs.docker.com/engine/install/debian/</a></li>
<li><a href="https://hub.docker.com/r/jenkins/jenkins/tags">[DockerHub 공식문서]</a> <a href="https://docs.docker.com/engine/install/debian/">https://docs.docker.com/engine/install/debian/</a></li>
</ul>
<p><code>Dockerfile</code></p>
<pre><code class="language-bash"># 기본 이미지 설정
FROM jenkins/jenkins:lts-jdk17

# root 계정으로 변경
USER root

# Docker 공식 GPG 키 추가 및 Docker 저장소 설정
RUN apt-get update &amp;&amp; \
    apt-get install -y ca-certificates curl &amp;&amp; \
    install -m 0755 -d /etc/apt/keyrings &amp;&amp; \
    curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc &amp;&amp; \
    chmod a+r /etc/apt/keyrings/docker.asc &amp;&amp; \
    echo &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
    $(. /etc/os-release &amp;&amp; echo &quot;$VERSION_CODENAME&quot;) stable&quot; | tee /etc/apt/sources.list.d/docker.list &gt; /dev/null

# Docker CLI 설치
RUN apt-get update &amp;&amp; \
    apt-get install -y docker-ce docker-ce-cli containerd.io

# 컨테이너 계정 및 그룹 권한 설정을 위한 변수
# ARG GROUP_GID=998
# ARG USER_UID=1000

# 컨테이너 계정 및 그룹 권한 설정
# RUN groupmod -g ${GROUP_GID} docker &amp;&amp; \ # docker 그룹 GID를 변경, 호스트와 일치
# RUN usermod -u ${USER_UID} jenkins &amp;&amp; \  # jenkins 계정 UID를 변경, 호스트와 일치
#     usermod -aG docker jenkins           # jenkins 계정을 docker 그룹에 추가

# jenkins 계정으로 변경 (Jenkins 기본 계정)
# USER jenkins</code></pre>
<blockquote>
</blockquote>
<p>컨테이너 사용자 (jenkins)가 호스트 파일에 접근 가능하도록
컨테이너 사용자 (jenkins) UID를 변경하여 호스트 설정과 일치시키고
컨테이너 그룹 (docker) GID를 변경하여 호스트 설정과 일치시킨다.
마지막으로 jenkins 계정을 docker 그룹에 추가한다.</p>
<blockquote>
</blockquote>
<p>참고로 UID와 GID는 아래와 같이 확인할 수 있다.
계정 UID는 호스트의 <code>/etc/passwd</code> 또는 <code>$ id -u [계정명]</code>
그룹 GID는 호스트의 <code>/etc/group</code> 또는 <code>$ getent group [그룹명]</code>
<a href="https://bitgadak.tistory.com/3">[Reference]</a> <a href="https://bitgadak.tistory.com/3">https://bitgadak.tistory.com/3</a></p>
<hr>
<h2 id="📖-shell-script-작성">📖 Shell Script 작성</h2>
<p><code>install_jenkins.sh</code></p>
<pre><code class="language-bash">#!/bin/bash

IMAGE_NAME=&quot;server/jenkins&quot;
CONTAINER_NAME=&quot;server-jenkins&quot;

IMAGE_ID=$(sudo docker images -q $IMAGE_NAME)
CONTAINER_ID=$(sudo docker ps -aqf &quot;name=$CONTAINER_NAME&quot;)

echo &quot;&gt;&gt;&gt; CURRENT DOCKER INFORMATION:&quot;
echo &quot;$IMAGE_NAME IMAGE_ID: $IMAGE_ID&quot;
echo -e &quot;$CONTAINER_NAME CONTAINER_ID: $CONTAINER_ID\n&quot;


# Stop &amp; Remove Existing Container
echo &quot;&gt;&gt;&gt; $CONTAINER_NAME 컨테이너 실행 여부 검사 시작...&quot;
if [ ! -z &quot;$CONTAINER_ID&quot; ]; then
    # docker stop $(docker ps -aq --filter &quot;name=$CONTAINER_NAME&quot;)
    # docker rm $(docker ps -ap --filter &quot;name=$CONTAINER_NAME&quot;)
    echo -e &quot;&gt;&gt;&gt; 실행중인 $CONTAINER_NAME 컨테이너 중지 및 삭제 시작...\n&quot;

    echo &quot;&gt;&gt;&gt; 실행중인 $CONTAINER_NAME 컨테이너 중지 시작...&quot;
    sudo docker stop $CONTAINER_ID || {
        echo &quot;&gt;&gt;&gt; $CONTAINER_NAME 컨테이너 중지 실패.&quot;
        exit 1
    }
    echo -e &quot;&gt;&gt;&gt; 실행중인 $CONTAINER_NAME 컨테이너 중지 완료.\n&quot;

    echo &quot;&gt;&gt;&gt; 중지상태인 $CONTAINER_NAME 컨테이너 삭제 시작...&quot;
    sudo docker rm $CONTAINER_ID || {
        echo &quot;&gt;&gt;&gt; $CONTAINER_NAME 컨테이너 삭제 실패.&quot;
        exit 1
    }
    echo -e &quot;&gt;&gt;&gt; 중지상태인 $CONTAINER_NAME 컨테이너 삭제 완료.\n&quot;

    echo &quot;&gt;&gt;&gt; 실행중인 $CONTAINER_NAME 컨테이너 중지 및 삭제 완료.&quot;
fi
echo -e &quot;&gt;&gt;&gt; $CONTAINER_NAME 컨테이너 실행 여부 검사 완료.\n&quot;


# Remove Existing Docker Image
echo &quot;&gt;&gt;&gt; $IMAGE_NAME 이미지 존재 여부 검사 시작...&quot;
if [ ! -z &quot;$IMAGE_ID&quot; ]; then
    # docker rmi -f $(docker image -q $IMAGE_NAME)
    echo &quot;&gt;&gt;&gt; 기존 $IMAGE_NAME 이미지 삭제 시작...&quot;
    sudo docker rmi $IMAGE_ID || {
        echo &quot;&gt;&gt;&gt; 기존 $IMAGE_NAME 이미지 삭제 실패.&quot;
        exit 1
    }
    echo &quot;&gt;&gt;&gt; 기존 $IMAGE_NAME 이미지 삭제 완료.&quot;
fi
echo -e &quot;&gt;&gt;&gt; $IMAGE_NAME 이미지 존재 여부 검사 완료.\n&quot;


# Build Docker Image

# 현재 사용자의 UID와 Docker 그룹의 GID 추출
GROUP_GID=$(getent group docker | cut -d: -f3)
USER_UID=$(id -u $USER)

# Docker 이미지를 빌드하면서 사용자 UID와 그룹 GID를 인자로 전달
echo &quot;&gt;&gt;&gt; $IMAGE_NAME 이미지 빌드 시작...&quot;
sudo docker build -t $IMAGE_NAME . \
  --build-arg USER_UID=$USER_UID \
  --build-arg GROUP_GID=$GROUP_GID || {
    echo &quot;&gt;&gt;&gt; $IMAGE_NAME 이미지 빌드 실패.&quot;
    exit 1
}
echo -e &quot;&gt;&gt;&gt; $IMAGE_NAME 이미지 빌드 완료.\n&quot;


# Run Docker Container (USER jenkins)
echo &quot;&gt;&gt;&gt; $CONTAINER_NAME 컨테이너 실행 시작...&quot;
sudo chown -R 1000:1000 /var/jenkins_home
sudo docker run -d \
    -p 8081:8080 -p 50000:50000 \
    -v /var/jenkins_home:/var/jenkins_home \
    -v /var/run/docker.sock:/var/run/docker.sock \
    --name $CONTAINER_NAME $IMAGE_NAME || {
        echo &quot;&gt;&gt;&gt; $CONTAINER_NAME 컨테이너 실행 실패.&quot;
        exit 1
    }
echo &quot;&gt;&gt;&gt; $CONTAINER_NAME 컨테이너 실행 완료.&quot;


## Run Docker Container (USER root)
#echo &quot;&gt;&gt;&gt; $CONTAINER_NAME 컨테이너 실행 시작...&quot;
#sudo docker run -d -u root \
#    -p 8081:8080 -p 50000:50000 \
#    -v /var/jenkins_home:/var/jenkins_home \
#    -v /var/run/docker.sock:/var/run/docker.sock \
#    --name $CONTAINER_NAME $IMAGE_NAME || {
#        echo &quot;&gt;&gt;&gt; $CONTAINER_NAME 컨테이너 실행 실패.&quot;
#        exit 1
#    }
#echo &quot;&gt;&gt;&gt; $CONTAINER_NAME 컨테이너 실행 완료.&quot;</code></pre>
<hr>
<h2 id="📖-shell-script-실행">📖 Shell Script 실행</h2>
<ul>
<li><code>Dockerfile</code>과 <code>Shell Script</code>가 같은 경로에 위치해야 함</li>
</ul>
<pre><code class="language-bash">$ mkdir jenkins
$ cd jenkins

$ sudo nano Dockerfile
$ sudo nano install_jenkins.sh

$ sudo chmod +x install_jenkins.sh
$ ./install_jenkins.sh</code></pre>
<hr>
<h1 id="📌-docker-jenkins-설정">📌 Docker Jenkins 설정</h1>
<ul>
<li><code>http://&lt;Your Domain or IP&gt;:8081</code></li>
<li>Jenkins 초기 비밀번호 확인</li>
<li>Jenkins 초기 접속 설정</li>
</ul>
<hr>
<h2 id="📖-jenkins-접속">📖 Jenkins 접속</h2>
<ul>
<li>아래 명령어들 중 하나를 선택해 초기 비밀번호 확인</li>
<li><code>/var/jenkins_home</code> 경로는 마운트된 볼륨이므로 호스트 서버에서도 확인 가능</li>
</ul>
<pre><code class="language-bash"># 호스트 서버에서 확인
$ sudo cat /var/jenkins_home/secrets/initialAdminPassword</code></pre>
<pre><code class="language-bash"># Docker Jenkins 처음 실행 로그로 확인
$ sudo docker logs server-jenkins</code></pre>
<pre><code class="language-bash"># Docker Jenkins 컨테이너에 접속하여 확인
$ sudo docker exec -it server-jenkins /bin/bash
$ cat /var/jenkins_home/secrets/initialAdminPassword</code></pre>
<pre><code class="language-bash"># Docker Jenkins 컨테이너에 명령어로 확인
$ sudo docker exec server-jenkins cat /var/jenkins_home/secrets/initialAdminPassword</code></pre>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/42d7a380-fe50-4ce4-b670-45e4af4e3119/image.png" alt=""></p>
<hr>
<h2 id="📖-jenkins-초기-설정">📖 Jenkins 초기 설정</h2>
<ul>
<li><code>Install suggested plugins</code> 선택</li>
<li>아이디, 비밀번호, 비밀번호 확인, 이름, 이메일 입력</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/36f8e1a6-6fb2-4e8b-985b-7a348b491c32/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/5ad63588-4749-4d3f-8090-9aafa3011b7f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/6e6e90ef-1938-41c4-bd63-d56b6b3b79b2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/7bfeeb1a-2dd6-488e-853d-48248213f5e9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/13f96957-e269-4c74-a3f7-18e2cf6ccbf8/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS/EC2] AWS EC2 Docker 설치]]></title>
            <link>https://velog.io/@sihoon_cho/AWSEC2-AWS-EC2-Docker-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@sihoon_cho/AWSEC2-AWS-EC2-Docker-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Mon, 11 Mar 2024 07:55:58 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 기반으로 작성되었습니다.
실습 위주의 이해를 목표로 하기 때문에 다소 과장이 많고 생략된 부분이 많을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 대해 유의하시기 바랍니다.

또한, 본 시리즈는 ChatGPT의 도움을 받아 작성되었습니다.
수 차례의 질문을 통해 도출된 여러가지 다양한 방식의 코드를 종합하여
작성자의 이해와 경험을 바탕으로 가장 정석으로 생각되는 코드를 재정립하였습니다.</code></pre><hr>
<h1 id="📌-docker-설치">📌 Docker 설치</h1>
<ul>
<li>2024 Docker 공식 문서 기반 설치 방법</li>
</ul>
<p><a href="https://docs.docker.com/engine/install/ubuntu/">[Reference]</a>: <a href="https://docs.docker.com/engine/install/ubuntu/">Docker Documents (https://docs.docker.com/engine/install/ubuntu/)</a></p>
<hr>
<h2 id="📖-1-docker-apt-repository-설정">📖 1. Docker apt repository 설정</h2>
<pre><code class="language-bash"># Add Docker&#39;s official GPG key:
$ sudo apt-get update
$ sudo apt-get install ca-certificates curl
$ sudo install -m 0755 -d /etc/apt/keyrings
$ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
$ sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
$ echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release &amp;&amp; echo &quot;$VERSION_CODENAME&quot;) stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null
$ sudo apt-get update</code></pre>
<hr>
<h2 id="📖-2-docker-패키지-설치">📖 2. Docker 패키지 설치</h2>
<pre><code class="language-bash">$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin</code></pre>
<hr>
<h2 id="📖-3-docker-패키지-설치-확인">📖 3. Docker 패키지 설치 확인</h2>
<pre><code class="language-bash">$ sudo docker run hello-world</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS/EC2] AWS EC2 인스턴스 시작]]></title>
            <link>https://velog.io/@sihoon_cho/AWSEC2-AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@sihoon_cho/AWSEC2-AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Sat, 02 Mar 2024 07:05:34 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 기반으로 작성되었습니다.
실습 위주의 이해를 목표로 하기 때문에 다소 과장이 많고 생략된 부분이 많을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 대해 유의하시기 바랍니다.

또한, 본 시리즈는 ChatGPT의 도움을 받아 작성되었습니다.
수 차례의 질문을 통해 도출된 여러가지 다양한 방식의 코드를 종합하여
작성자의 이해와 경험을 바탕으로 가장 정석으로 생각되는 코드를 재정립하였습니다.</code></pre><hr>
<h1 id="📌-ec2-프리티어-요금">📌 EC2 프리티어 요금</h1>
<ul>
<li>AWS(Amazon Web Services) 가입 후 12개월 간 프리티어 제공</li>
<li>프리티어는 무제한 무료가 아닌 조건부 무료임에 주의<ul>
<li><code>EC2</code>: 계정 당 매월 750시간 무료</li>
<li><code>EBS</code>: 계정 당 30GB 무료</li>
<li><code>RDS</code>: 계정 당 20GB 한도 내에서 매월 750시간 무료</li>
<li><code>S3</code>: 계정 당 5GB 무료, GET 요청 20,000건 무료, PUT 요청 2,000건 무료</li>
<li><code>탄력적 IP</code>: 인스턴스 당 1개의 탄력적 IP 무료, 단 실행중인 인스턴스에 한함</li>
</ul>
</li>
<li>아래의 안내사항을 정독하고, 과금발생시 아래의 링크를 참고하여 문제해결</li>
</ul>
<blockquote>
<p><strong>[ AWS 공식 프리티어 요금안내 ]</strong>
<a href="https://aws.amazon.com/ko/free/?nc2=h_ql_pr_ft&amp;all-free-tier.sort-by=item.additionalFields.SortRank&amp;all-free-tier.sort-order=asc&amp;awsf.Free%20Tier%20Types=*all&amp;awsf.Free%20Tier%20Categories=*all">https://aws.amazon.com/ko/free/?nc2=h_ql_pr_ft&amp;all-free-tier.sort-by=item.additionalFields.SortRank&amp;all-free-tier.sort-order=asc&amp;awsf.Free%20Tier%20Types=*all&amp;awsf.Free%20Tier%20Categories=*all</a></p>
</blockquote>
<blockquote>
<p><strong>[ AWS 서비스 별 프리티어 과금안내 ]</strong>
<a href="https://inpa.tistory.com/entry/AWS-%F0%9F%92%B0-%ED%94%84%EB%A6%AC%ED%8B%B0%EC%96%B4-%EC%9A%94%EA%B8%88-%ED%8F%AD%ED%83%84-%EB%B0%A9%EC%A7%80-%F0%9F%92%B8-%EB%AC%B4%EB%A3%8C-%EC%82%AC%EC%9A%A9%EB%9F%89-%EC%A0%95%EB%A6%AC#s3">https://inpa.tistory.com/entry/AWS-%F0%9F%92%B0-%ED%94%84%EB%A6%AC%ED%8B%B0%EC%96%B4-%EC%9A%94%EA%B8%88-%ED%8F%AD%ED%83%84-%EB%B0%A9%EC%A7%80-%F0%9F%92%B8-%EB%AC%B4%EB%A3%8C-%EC%82%AC%EC%9A%A9%EB%9F%89-%EC%A0%95%EB%A6%AC#s3</a></p>
</blockquote>
<blockquote>
<p><strong>[ AWS EC2 요금제 및 월별요금 계산법  ]</strong>
<a href="https://duckracoon.tistory.com/entry/AWS-%EC%9A%94%EA%B8%88%EC%A0%95%EB%A6%AC">https://duckracoon.tistory.com/entry/AWS-%EC%9A%94%EA%B8%88%EC%A0%95%EB%A6%AC</a>
<strong>[ AWS EBS/RDS/ElasticIP 프리티어 과금예시 ]</strong>
<a href="https://gun0912.tistory.com/45">https://gun0912.tistory.com/45</a>
<strong>[ AWS 프리티어 사용량 확인 ]</strong>
<a href="https://hnev.tistory.com/47">https://hnev.tistory.com/47</a>
<strong>[ RDS DB 백업 스토리지 스냅샷 제거 및 탄력적 IP 삭제 ]</strong>
<a href="https://blog.naver.com/baekmg1988/221608368784">https://blog.naver.com/baekmg1988/221608368784</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/ffe55b52-1d25-49b7-9570-cdd6ad6facf0/image.png" alt=""></p>
<hr>
<h1 id="📌-ec2-프리티어-인스턴스-생성">📌 EC2 프리티어 인스턴스 생성</h1>
<blockquote>
<p><strong>[ EC2 인스턴스 생성 및 SSH 접속 ]</strong>
<a href="https://ksh-coding.tistory.com/72">https://ksh-coding.tistory.com/72</a></p>
</blockquote>
<h2 id="📖-인스턴스-시작-버튼-클릭">📖 인스턴스 시작 버튼 클릭</h2>
<ul>
<li>AWS EC2 리전(Region, 지역 ) 서울로 변경</li>
<li>인스턴스 시작 버튼 클릭</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/64c9a755-3c1c-4e49-976a-fe6737f89fa3/image.png" alt=""></p>
<h2 id="📖-인스턴스-설정">📖 인스턴스 설정</h2>
<ul>
<li>인스턴스 이름 입력</li>
<li>애플리케이션 및 OS 이미지(Amazon Machine Image, AMI) 선택</li>
<li>인스턴스 유형 선택</li>
<li>키 페어 생성</li>
<li>네트워크 설정</li>
<li>스토리지 구성</li>
<li>인스턴스 생성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/c769ac0b-d724-4c8d-b80b-3120512d3198/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/2d16cf99-f84e-451d-ba4b-ee967bc38e00/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/884a2d3b-4637-4d23-989a-ae790ae4f93f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/74f519dd-3c6e-4b3e-9ba3-3fcfbfd74691/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/84d587ba-773e-4f11-908e-c145d4d3dcc5/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/0d87107c-9e5e-4a7d-be14-8bb532f85ea3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/53514c4f-7713-4575-8aa0-a471b0796501/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/1a4b3464-ea19-4c35-9e21-bdbcf0972dba/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/0f5b71d0-4aec-4fa9-b4d9-1bdbbfdd3c48/image.png" alt=""></p>
<hr>
<h1 id="📌-탄력적-ip-고정-ip-설정">📌 탄력적 IP (고정 IP) 설정</h1>
<ul>
<li>탄력적 IP</li>
<li>탄력적 IP 주소 할당</li>
<li>탄력적 IP 주소 연결</li>
</ul>
<h2 id="📖-탄력적-ip-주소-할당">📖 탄력적 IP 주소 할당</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/261a35fc-a578-43fd-80d2-9372b8b5a445/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/4b94aab0-fc35-4578-94fd-714a3f46dc61/image.png" alt=""></p>
<h2 id="📖-탄력적-ip-주소-연결">📖 탄력적 IP 주소 연결</h2>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/f900d932-0816-4f9b-9c60-880d8506bb9e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/857f8e27-b393-4fd7-ab9e-d8435c0547d1/image.png" alt=""></p>
<hr>
<h1 id="📌-windows-pemkey-권한-설정">📌 Windows Pemkey 권한 설정</h1>
<ul>
<li>Windows11에서 바로 사용 가능</li>
<li>Windows10에서 권한 수정 후 사용 가능<ul>
<li>마우스 우클릭 - 속성 - 보안 탭 - 고급</li>
<li>상속 사용 안 함 - 이 개체에서 상속된 사용 권한을 모두 제거합니다.</li>
<li>추가 - 보안 주체 선택 - 윈도우 계정명 - 확인 - 확인 - 확인 - 확인</li>
</ul>
</li>
</ul>
<pre><code class="language-bash">$ cd ./Desktop/{keypair_downloaded_path}
$ ssh -i YourKeyPairName.pem ubuntu@YourElasticIP</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS/EC2] AWS EC2 초기 설정]]></title>
            <link>https://velog.io/@sihoon_cho/AWS-EC2-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@sihoon_cho/AWS-EC2-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Fri, 01 Mar 2024 11:40:13 GMT</pubDate>
            <description><![CDATA[<h1 id="-읽기에-앞서">※ 읽기에 앞서</h1>
<hr>
<pre><code>본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 기반으로 작성되었습니다.
실습 위주의 이해를 목표로 하기 때문에 다소 과장이 많고 생략된 부분이 많을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 대해 유의하시기 바랍니다.

또한, 본 시리즈는 ChatGPT의 도움을 받아 작성되었습니다.
수 차례의 질문을 통해 도출된 여러가지 다양한 방식의 코드를 종합하여
작성자의 이해와 경험을 바탕으로 가장 정석으로 생각되는 코드를 재정립하였습니다.</code></pre><hr>
<h1 id="📌-서버-시간-변경">📌 서버 시간 변경</h1>
<pre><code class="language-bash">$ sudo timedatectl set-timezone Asia/Seoul
$ timedatectl
$ date</code></pre>
<hr>
<h1 id="📌-미러-서버-변경">📌 미러 서버 변경</h1>
<blockquote>
<p>미러 서버는 데비안 계열 리눅스(Debian, Ubuntu)에서 
시스템에 설치할 수 있는 deb 패키지들을 가지고 있는 서버이며,
이 서버에 빠르게 접근할 수 있도록 서버를 통째로 복제해둔 로컬 서버입니다.</p>
</blockquote>
<ul>
<li>초기 기본 설정된 우분투 패키지 미러서버는 상당히 느림<ul>
<li><code>$ sudo apt update</code> 등 패키지 다운로드시 불편</li>
<li>패키지 다운로드 속도를 향상시키기 위해 미러서버 변경</li>
</ul>
</li>
<li>초기 기본 설정된 우분투 패키지 미러서버(EC2 인스턴스에 따라 다를 수 있음):<ul>
<li><code>http://kr.archive.ubuntu.com/ubuntu/</code></li>
<li><code>http://ap-southeast-2.ec2.archive.ubuntu.com/ubuntu/</code></li>
<li><code>http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu/</code></li>
</ul>
</li>
<li>변경 가능한 우분투의 미러서버를 검색할 수 있는 사이트<ul>
<li><code>cdmirrors</code>: Ubuntu 설치용 CD 이미지 배포 저장소<ul>
<li><a href="https://launchpad.net/ubuntu/+cdmirrors">https://launchpad.net/ubuntu/+cdmirrors</a></li>
</ul>
</li>
<li><code>archivemirrors</code>: Ubuntu 패키지 저장소<ul>
<li><a href="https://launchpad.net/ubuntu/+archivemirrors">https://launchpad.net/ubuntu/+archivemirrors</a></li>
</ul>
</li>
</ul>
</li>
<li>국내 변경 가능한 우분투 미러서버:<ul>
<li><code>http://ftp.kaist.ac.kr/ubuntu/</code></li>
<li><code>http://mirror.kakao.com/ubuntu/</code></li>
</ul>
</li>
</ul>
<pre><code class="language-bash"># 문자열 변경 명령어
:%s/원본 문장/변경 문장</code></pre>
<pre><code class="language-bash"># 카이스트 미러서버 변경
$ sudo vi /etc/apt/sources.list
:%s/kr.archive.ubuntu.com/ftp.kaist.ac.kr/
:%s/ap-southeast-2.ec2.archive.ubuntu.com/ftp.kaist.ac.kr/
:%s/ap-northeast-2.ec2.archive.ubuntu.com/ftp.kaist.ac.kr/</code></pre>
<pre><code class="language-bash"># 카카오 미러서버 변경
$ sudo vi /etc/apt/sources.list
$ :%s/kr.archive.ubuntu.com/mirror.kakao.com/
$ :%s/ap-southeast-2.ec2.archive.ubuntu.com/mirror.kakao.com/
$ :%s/ap-northeast-2.ec2.archive.ubuntu.com/mirror.kakao.com/</code></pre>
<hr>
<h1 id="📌-패키지-업데이트">📌 패키지 업데이트</h1>
<ul>
<li>PPA(Personal Package Archive): 개인 패키지 저장소 = 비공식 패키지 저장소 <ul>
<li><code>Launchpad</code>에서 제공하는 우분투 공식 패키지 저장소에는 없는
서드 파티 소프트웨어를 위한 개인용 소프트웨어 패키지 저장소</li>
</ul>
</li>
</ul>
<pre><code class="language-bash">$ sudo apt update
$ sudo apt upgrade
$ sudo add-apt-repository --remove ppa:certbot/certbot</code></pre>
<hr>
<h1 id="📌-가상-메모리-할당">📌 가상 메모리 할당</h1>
<ul>
<li>Swap 메모리는 하드디스크의 일부를 RAM처럼 사용하도록 만들어진 메모리</li>
<li>HDD 장치를 사용하는 것이기 때문에 읽고 쓰는 속도가 급격히 느려짐</li>
<li>일반적으로 RAM 용량의 1배 ~ 2배를 Swap 메모리로 권장</li>
</ul>
<blockquote>
<p>[AWS EC2 Swap 메모리 할당방법]
<a href="https://repost.aws/knowledge-center/ec2-memory-swap-file">https://repost.aws/knowledge-center/ec2-memory-swap-file</a>
[AWS EC2 Swap 메모리 권장크기]
<a href="https://help.ubuntu.com/community/SwapFaq#How_much_swap_do_I_need.3F">https://help.ubuntu.com/community/SwapFaq#How_much_swap_do_I_need.3F</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sihoon_cho/post/f4673fc5-9cc5-4109-89d2-277de868e205/image.png" alt=""></p>
<h2 id="📖-메모리-확인">📖 메모리 확인</h2>
<pre><code class="language-bash"># 메모리 사용량, 여유량, 캐시 크기
$ free
$ free -b # 메모리 크기를 바이트 단위로 출력
$ free -k # 메모리 크기를 키비바이트 단위로 출력 
$ free -m # 메모리 크기를 메비바이트 단위로 출력
$ free -g # 메모리 크기를 기비바이트 단위로 출력
$ free -h # 메모리 크기를 사람이 읽기 쉬운 단위로 출력</code></pre>
<h2 id="📖-swap-메모리-비활성화">📖 Swap 메모리 비활성화</h2>
<pre><code class="language-bash"># 활성화 중인 Swap 메모리가 있다면 비활성화
$ sudo free -m
$ sudo swapon -s
$ sudo swapoff -a
$ sudo swapoff -v /swapfile</code></pre>
<h2 id="📖-swap-메모리-할당-방법-1">📖 Swap 메모리 할당 방법 1</h2>
<pre><code class="language-bash"># In this example dd command, the swapfile is 2 GB (64 MB x 32):
$ sudo dd if=/dev/zero of=/swapfile bs=64M count=32
$ sudo chmod 600 /swapfile        # 권한 수정
$ sudo mkswap /swapfile           # swapfile 생성
$ sudo swapon /swapfile           # swapfile 활성화
$ sudo nano /etc/fstab            # 파일 편집
/swapfile swap swap defaults 0 0  # 내용 추가 (권장)
/swapfile none swap sw 0 0        # 내용 추가 (옵션)

# [파일시스템장치] [마운트포인트] [파일시스템 종류] [옵션] [dump설정] [파일점검옵션]
    /swapfile        swap          swap     defaults    0          0
    /swapfile        none          swap        sw       0          0</code></pre>
<pre><code class="language-bash"># linux/ubuntu dd command

# In this example dd command, the swapfile is 1 GB (64 MB x 16):
# In this example dd command, the swapfile is 2 GB (64 MB x 32):
# In this example dd command, the swapfile is 4 GB (128 MB x 32):
# In this example dd command, the swapfile is 8 GB (128 MB x 64):
$ sudo dd if=/dev/zero of=/swapfile bs=64M count=16
$ sudo dd if=/dev/zero of=/swapfile bs=64M count=32
$ sudo dd if=/dev/zero of=/swapfile bs=128M count=32
$ sudo dd if=/dev/zero of=/swapfile bs=128M count=64

# if [File] = 지정한 파일을 입력대상으로 설정
# of [File] = 지정한 파일을 출력대상으로 설정
# bs [Bytes] = 한 번에 읽고 쓸 최대 바이트 크기 지정
# count [Number Blocks] = 지정한 블록 수 만큼 복사
# /dev/zero 내용을 읽고, /swapfile 파일에 쓰기, 64M 크기로 16번 
</code></pre>
<h2 id="📖-swap-메모리-할당-방법-2">📖 Swap 메모리 할당 방법 2</h2>
<ul>
<li><code>fallocate</code>를 사용하여 빈 파일을 생성하는 것은, <code>dd</code>를 사용하는 것보다 더 간단하고 빠름</li>
<li>큰 파일을 생성할 때 성능 상 이점이 있으나, 파일 시스템이 <code>fallocate</code>를 지원해야 하며,
지원하지 않는 파일 시스템에서는 사용할 수 없음</li>
</ul>
<pre><code class="language-bash">$ sudo fallocate -l 8G /swapfile  # Swap 메모리 할당 8GB
$ sudo chmod 600 /swapfile        # 권한 수정
$ sudo mkswap /swapfile           # swapfile 생성
$ sudo swapon /swapfile           # swapfile 활성화
$ sudo nano /etc/fstab            # 파일 편집
/swapfile swap swap defaults 0 0  # 내용 추가 (옵션)
/swapfile none swap sw 0 0        # 내용 추가 (권장)

# [파일시스템장치] [마운트포인트] [파일시스템 종류] [옵션] [dump설정] [파일점검옵션]
    /swapfile        swap          swap     defaults    0          0
    /swapfile        none          swap        sw       0          0</code></pre>
<ul>
<li><code>마운트 포인트</code> 필드: 일반적으로 swap 파일의 경우 마운트 포인트로 <code>none</code>을 사용합니다.
swap 파일 또는 파티션은 실제 파일 시스템 마운트 포인트에 위치하지 않습니다.</li>
<li><code>옵션</code> 필드: <code>defaults</code> 또는 <code>sw</code>와 같은 옵션들이 있으며, swap에 관련된 옵션을 지정합니다.<ul>
<li><code>defaults</code>: 파일 시스템에 대한 기본 마운트 옵션을 의미합니다. swap의 경우, 특별한 마운트 옵션이 필요 없기 때문에 이 옵션은 실질적으로 무시될 수 있습니다.</li>
<li><code>sw</code>: swap 파일이나 파티션을 활성화하기 위해 사용되는 옵션이며, swapon 명령어에 해당하는 옵션입니다. 일반적으로 swap 항목에서는 sw 옵션이 더 명확한 의미를 가집니다.</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>결론 - 일반적으로 사용되는 방법</strong>
<code>/swapfile none swap sw 0 0</code>이 <code>/swapfile swap swap defaults 0 0</code>보다 더 명확하고 표준적인 표현으로, swapfile 이나 파티션을 <code>/etc/fstab</code>에 추가할 때 일반적으로 사용됩니다.
<code>none</code>을 사용하는 것이 마운트 포인트가 필요 없는 swap에 더 적합하며,
<code>sw</code> 옵션은 swap 활성화를 명시적으로 나타냅니다.</p>
</blockquote>
<h2 id="📖-swap-메모리-삭제">📖 Swap 메모리 삭제</h2>
<pre><code class="language-bash"># swapfile 삭제
$ sudo free -m
$ sudo swapon -s
$ sudo swapoff -a                 # 모두 비활성화
$ sudo swapoff -v /swapfile       # swapfile 비활성화
$ sudo nano /etc/fstab
/swapfile swap swap defaults 0 0  # 내용 삭제
$ sudo rm /swapfile               # swapfile 삭제</code></pre>
<hr>
<h1 id="📌-킬로바이트kb와-키비바이트kib">📌 킬로바이트(KB)와 키비바이트(KiB)</h1>
<blockquote>
<p>SI 접두어: 국제단위계(International System of Units; 약칭 SI)에서
각 단위의 양의 크기를 쉽게 나타내기 위해 각 단위의 앞에 붙여 쓰는 접두어</p>
</blockquote>
<ul>
<li><p><strong><code>킬로바이트(KiloByte)</code></strong>: 킬로 바이트, SI 접두어</p>
<pre><code>1 KB = 10³ bytes = 1000 bytes</code></pre></li>
<li><p><strong><code>키비바이트(KibiByte)</code></strong>: 킬로 이진 바이트, Kilo Binary Byte의 준말</p>
<pre><code>1 KiB = 2¹⁰ bytes = 1024 bytes</code></pre></li>
<li><p><a href="https://80000coding.oopy.io/c51ad838-cde0-4f1e-b93b-7dd8d60ef6a2">https://80000coding.oopy.io/c51ad838-cde0-4f1e-b93b-7dd8d60ef6a2</a>
<img src="https://velog.velcdn.com/images/sihoon_cho/post/b29ff8e2-9035-455c-b229-05832bab8867/image.png" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] 스프링부트 스케줄러 @Scheduled, @EnableSchedule]]></title>
            <link>https://velog.io/@sihoon_cho/SpringBoot-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC-Scheduled-EnableSchedule</link>
            <guid>https://velog.io/@sihoon_cho/SpringBoot-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC-Scheduled-EnableSchedule</guid>
            <pubDate>Sat, 15 Jul 2023 12:50:24 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-enablescheduling">📌 @EnableScheduling</h1>
<hr>
<ul>
<li>Main Application Class에 <code>@EnableScheduling</code> 추가</li>
<li><code>org.springframework.boot:spring-boot-starter</code> 기본 내장</li>
</ul>
<pre><code class="language-java">package com.project.projectname;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class ProjectNameApplication {

    public static void main(String[] args) {
        SpringApplication.run(rojectNameApplication.class, args);
    }

}</code></pre>
<ul>
<li><code>@Scheduled</code> 사용 규칙<ul>
<li>Class에 <code>@Component</code></li>
<li>Method에 <code>@Scheduled</code><ul>
<li>Method는 void 타입</li>
<li>Method는 매개변수 사용불가</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<h1 id="📌-scheduled">📌 @Scheduled</h1>
<hr>
<ul>
<li><code>@Scheduled</code> 병렬 사용 방법<ul>
<li>Class에는 <code>@EnableAsync</code> 추가</li>
<li>Method에는 <code>@Async</code> 추가</li>
</ul>
</li>
</ul>
<h2 id="📖-fixeddelay--fixeddelaystring">📖 fixedDelay &amp; fixedDelayString</h2>
<hr>
<ul>
<li>이전 작업이 완료될 때까지 대기</li>
<li>메서드 종료시간 기준, <code>milliseconds</code> 간격으로 실행</li>
<li>하나의 인스턴스만 항상 실행되도록 해야 할 상황에서 유용</li>
<li><code>milliseconds</code> 타입에 따라 정수이면 <code>fixedDelay</code> 문자열이면 <code>fixedDelayString</code></li>
</ul>
<pre><code class="language-java">@Scheduled(fixedDelay = 1000)
public void scheduledFixedDelayTest() throws InterruptedException {
    log.info(&quot;Fixed Delay Test: {}&quot;, System.currentTimeMillis() / 1000);
    Thread.sleep(5000);
}</code></pre>
<pre><code class="language-java">// 문자열 milliseconds를 사용할 때
@Scheduled(fixedDelayString = &quot;${fixedDelay.in.milliseconds}&quot;)
public void scheduledFixedDelayStringTest() throws InterruptedException {
    log.info(&quot;Fixed Delay String Test: {}&quot;, System.currentTimeMillis() / 1000);
    Thread.sleep(5000);
}</code></pre>
<h2 id="📖-fixedrate--fixedratestring">📖 fixedRate &amp; fixedRateString</h2>
<hr>
<ul>
<li>이전 작업이 완료될 때까지 대기</li>
<li>메서드 시작시간 기준, <code>milliseconds</code> 간격으로 실행</li>
<li>모든 실행이 독립적인 경우에 유용</li>
<li><code>milliseconds</code> 타입에 따라 정수이면 <code>fixedRate</code> 문자열이면 <code>fixedRateString</code></li>
</ul>
<pre><code class="language-java">@Scheduled(fixedRate = 1000)
public void scheduledFixedRateTest() throws InterruptedException {
    log.info(&quot;Fixed Rate Test: {}&quot;, System.currentTimeMillis() / 1000);
    Thread.sleep(5000);
}</code></pre>
<pre><code class="language-java">// 문자열 milliseconds를 사용할 때
@Scheduled(fixedRateString = &quot;${fixedRate.in.milliseconds}&quot;)
public void scheduledFixedRateStringTest() throws InterruptedException {
    log.info(&quot;Fixed Rate String Test: {}&quot;, System.currentTimeMillis() / 1000);
    Thread.sleep(5000);
}</code></pre>
<h2 id="📖-initialdelay--initialdelaystring">📖 initialDelay &amp; initialDelayString</h2>
<hr>
<ul>
<li>초기 지연 시간으로 최초 실행시에만 적용되는 지연 시간</li>
<li><code>fixedDelay</code> 혹은 <code>fixedRate</code> 등의 고정 시간값과 함께 사용</li>
<li><code>initialDelay</code> 시간 이후 최초 실행, 이후 <code>fixedDelay</code> 시간마다 실행</li>
<li><code>milliseconds</code> 타입에 따라 정수이면 <code>initialDelay</code> 문자열이면 <code>initialDelayString</code></li>
</ul>
<pre><code class="language-java">// 1초 대기 후 메서드 종료시간 기준으로 5초마다 반복
@Scheduled(fixedDelay = 5000, initialDelay = 1000)
public void scheduledFixedDelayWithInitialDelayTest() {
    long now = System.currentTimeMillis() / 1000;
    log.info(&quot;Initial Delay with Fixed Delay Test: {}&quot;, now);
}</code></pre>
<pre><code class="language-java">// 1초 대기 후 메서드 시작시간 기준으로 5초마다 반복
@Scheduled(fixedRate = 5000, initialDelayString = &quot;1000&quot;)
public void scheduledFixedRateWithInitialDelayTest() {
    long now = System.currentTimeMillis() / 1000;
    log.info(&quot;Initial Delay with Fixed Rate Test: {}&quot;, now);
}</code></pre>
<h2 id="📖-cron--zone">📖 Cron &amp; Zone</h2>
<hr>
<ul>
<li><code>Cron</code> 표현식을 통해 매 특정 시점마다 실행<ul>
<li><code>Cron</code>: <code>&quot;* * * * * *&quot;</code></li>
<li>초(0-59) 분(0-59) 시간(0-23) 일(1-31) 월(1-12) 요일(0-7)</li>
</ul>
</li>
<li><code>Zone</code> = <code>&quot;Asia/Seoul&quot;</code><ul>
<li><code>Default</code>는 <code>Local</code> 시간대 사용</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Scheduled(cron = &quot;0 15 10 15 * ?&quot;) // 매월 15일 오전 10시 15분 실행
// @Scheduled(cron = &quot;0 15 10 15 11 ?&quot;) // 11월 15일 오전 10시 15분 실행
// @Scheduled(cron = &quot;${cron.expression}&quot;)
public void scheduledCronExpressionTest() {
    long now = System.currentTimeMillis() / 1000;
    log.info(&quot;Scheduled with Cron Expression Test: {}&quot;, now);
}</code></pre>
<pre><code class="language-java">// 유럽/파리 시간 기준 매월 15일 오전 10시 15분 실행
@Scheduled(cron = &quot;0 15 10 15 * ?&quot;, zone = &quot;Europe/Paris&quot;)
public void scheduledCronExpressionWithZoneTest() {
    long now = System.currentTimeMillis() / 1000;
    log.info(&quot;Scheduled with Cron Expression and Zone Test: {}&quot;, now);
}</code></pre>
<br>

<h1 id="📌-summary">📌 Summary</h1>
<hr>
<h3 id="📋-fixeddelay--fixeddelaystring">📋 fixedDelay &amp; fixedDelayString</h3>
<hr>
<ul>
<li>이전 작업이 완료될 때까지 대기</li>
<li>메서드 종료시간 기준, <code>milliseconds</code> 간격으로 실행</li>
<li>하나의 인스턴스만 항상 실행되도록 해야 할 상황에서 유용</li>
<li><code>milliseconds</code> 타입에 따라 정수이면 <code>fixedDelay</code> 문자열이면 <code>fixedDelayString</code></li>
</ul>
<h3 id="📋-fixedrate--fixedratestring">📋 fixedRate &amp; fixedRateString</h3>
<hr>
<ul>
<li>이전 작업이 완료될 때까지 대기</li>
<li>메서드 시작시간 기준, <code>milliseconds</code> 간격으로 실행</li>
<li>모든 실행이 독립적인 경우에 유용</li>
<li><code>milliseconds</code> 타입에 따라 정수이면 <code>fixedRate</code> 문자열이면 <code>fixedRateString</code></li>
</ul>
<h3 id="📋-initialdelay--initialdelaystring">📋 initialDelay &amp; initialDelayString</h3>
<hr>
<ul>
<li>초기 지연 시간으로 최초 실행시에만 적용되는 지연 시간</li>
<li><code>fixedDelay</code> 혹은 <code>fixedRate</code> 등의 고정 시간값과 함께 사용</li>
<li><code>initialDelay</code> 시간 이후 최초 실행, 이후 <code>fixedDelay</code> 시간마다 실행</li>
<li><code>milliseconds</code> 타입에 따라 정수이면 <code>initialDelay</code> 문자열이면 <code>initialDelayString</code></li>
</ul>
<h3 id="📋-cron--zone">📋 Cron &amp; Zone</h3>
<hr>
<ul>
<li><code>Cron</code> 표현식을 통해 매 특정 시점마다 실행<ul>
<li><code>Cron</code>: <code>&quot;* * * * * *&quot;</code></li>
<li>초(0-59) 분(0-59) 시간(0-23) 일(1-31) 월(1-12) 요일(0-7)</li>
</ul>
</li>
<li><code>Zone</code> = <code>&quot;Asia/Seoul&quot;</code><ul>
<li><code>Default</code>는 <code>Local</code> 시간대 사용</li>
</ul>
</li>
</ul>
<br>

<h1 id="📌-test">📌 Test</h1>
<hr>
<pre><code class="language-java">import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component // Class에는 @Component 추가
@EnableAsync // Class에는 @EnableAsync 추가
public class ScheduleTest {

    /**
     * 이전 작업이 완료될 때까지 대기
     * 메서드 종료시간 기준, `milliseconds` 간격으로 실행
     * 하나의 인스턴스만 항상 실행되도록 해야 할 상황에서 유용
     */
    @Scheduled(fixedDelay = 1000)
    public void scheduledFixedDelayTest() throws InterruptedException {
        log.info(&quot;Fixed Delay Test: {}&quot;, System.currentTimeMillis() / 1000);
        Thread.sleep(5000);
    }

    // 문자열 milliseconds를 사용할 때
    @Scheduled(fixedDelayString = &quot;${fixedDelay.in.milliseconds}&quot;)
    public void scheduledFixedDelayStringTest() throws InterruptedException {
        log.info(&quot;Fixed Delay String Test: {}&quot;, System.currentTimeMillis() / 1000);
        Thread.sleep(5000);
    }


    /**
     * 이전 작업이 완료될 때까지 대기
     * 메서드 시작시간 기준, `milliseconds` 간격으로 실행
     * 모든 실행이 독립적인 경우에 유용
     */
    @Async // Method에는 @Async 추가
    @Scheduled(fixedRate = 1000)
    public void scheduledFixedRateTest() throws InterruptedException {
        log.info(&quot;Fixed Rate Test: {}&quot;, System.currentTimeMillis() / 1000);
        Thread.sleep(5000);
    }

    // 문자열 milliseconds를 사용할 때
    @Async // Method에는 @Async 추가
    @Scheduled(fixedRateString = &quot;${fixedRate.in.milliseconds}&quot;)
    public void scheduledFixedRateStringTest() throws InterruptedException {
        log.info(&quot;Fixed Rate String Test: {}&quot;, System.currentTimeMillis() / 1000);
        Thread.sleep(5000);
    }


    /**
     * 초기 지연 시간으로 최초 실행시에만 적용되는 지연 시간
     * `fixedDelay` 혹은 `fixedRate` 등의 고정 시간값과 함께 사용
     * `initialDelay` 시간 이후 최초 실행, 이후 `fixedDelay` 시간마다 실행
     */
    // 1초 대기 후 메서드 종료시간 기준으로 5초마다 반복
    @Scheduled(fixedDelay = 5000, initialDelay = 1000)
    public void scheduledFixedDelayWithInitialDelayTest() {
        long now = System.currentTimeMillis() / 1000;
        log.info(&quot;Initial Delay with Fixed Delay Test: {}&quot;, now);
    }

    // 1초 대기 후 메서드 시작시간 기준으로 5초마다 반복
    @Scheduled(fixedRate = 5000, initialDelayString = &quot;1000&quot;)
    public void scheduledFixedRateWithInitialDelayTest() {
        long now = System.currentTimeMillis() / 1000;
        log.info(&quot;Initial Delay with Fixed Rate Test: {}&quot;, now);
    }


    /**
     * `Cron` 표현식을 통해 매 특정 시점마다 실행
     * * `Cron`: `&quot;* * * * * *&quot;`
     * * 초(0-59) 분(0-59) 시간(0-23) 일(1-31) 월(1-12) 요일(0-7)
     * `Zone` = `&quot;Asia/Seoul&quot;`
     * * `Default`는 `Local` 시간대 사용
     */
    @Scheduled(cron = &quot;0 15 10 15 * ?&quot;) // 매월 15일 오전 10시 15분 실행
    // @Scheduled(cron = &quot;0 15 10 15 11 ?&quot;) // 11월 15일 오전 10시 15분 실행
    // @Scheduled(cron = &quot;${cron.expression}&quot;)
    public void scheduledCronExpressionTest() {
        long now = System.currentTimeMillis() / 1000;
        log.info(&quot;Scheduled with Cron Expression Test: {}&quot;, now);
    }

    // 유럽/파리 시간 기준 매월 15일 오전 10시 15분 실행
    @Scheduled(cron = &quot;0 15 10 15 * ?&quot;, zone = &quot;Europe/Paris&quot;)
    public void scheduledCronExpressionWithZoneTest() {
        long now = System.currentTimeMillis() / 1000;
        log.info(&quot;Scheduled with Cron Expression and Zone Test: {}&quot;, now);
    }

}</code></pre>
<h1 id="📌-reference">📌 Reference</h1>
<hr>
<p><a href="https://data-make.tistory.com/699">https://data-make.tistory.com/699</a></p>
]]></description>
        </item>
    </channel>
</rss>