<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>haxxru</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 06 Jul 2026 00:37:22 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>haxxru</title>
            <url>https://velog.velcdn.com/images/xxxhyxxk__/profile/df239db7-4e39-46ef-903a-f0e4b9266eac/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. haxxru. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/xxxhyxxk__" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Day 70 - Docker pt.2]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-70-Docker-pt.2</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-70-Docker-pt.2</guid>
            <pubDate>Mon, 06 Jul 2026 00:37:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 07월 01일 작성된 글입니다.</p>
</blockquote>
<p>Docker를 학습하며 이미지 빌드, 레이어와 캐시, 데이터베이스 컨테이너, Spring Boot 배포, 멀티 스테이지 빌드까지 학습했다. 특히 Docker를 이용한 실제 배포 환경과 이미지 최적화 방법을 중심으로 정리했다.</p>
<hr>
<h1 id="1-docker-이미지와-캐시">1. Docker 이미지와 캐시</h1>
<p>Docker 이미지는 여러 개의 레이어(Layer)로 구성되며, 각 레이어는 변경할 수 없는(Immutable) 구조를 가진다.</p>
<h2 id="핵심">핵심</h2>
<ul>
<li>Dockerfile의 명령어마다 새로운 레이어 생성</li>
<li>변경되지 않은 레이어는 캐시 재사용</li>
<li>변경된 레이어 이후는 다시 빌드</li>
<li>자주 변경되는 내용은 아래쪽에 배치</li>
</ul>
<pre><code class="language-dockerfile">FROM nginx:latest
COPY source/web /web
RUN apt-get update</code></pre>
<hr>
<h1 id="2-여러-사이트를-포함한-이미지-생성">2. 여러 사이트를 포함한 이미지 생성</h1>
<p>하나의 Nginx 이미지에서 여러 사이트를 동시에 서비스할 수 있다.</p>
<h2 id="핵심-1">핵심</h2>
<ul>
<li>하나의 이미지에 site1, site2, site3 포함</li>
<li>여러 포트를 각각 다른 사이트에 연결</li>
<li>설정 변경 후 이미지 재빌드</li>
</ul>
<pre><code class="language-dockerfile">COPY source/web /web
COPY vhost.conf /etc/nginx/conf.d/</code></pre>
<hr>
<h1 id="3-docker-history">3. docker history</h1>
<p>이미지가 어떤 레이어로 만들어졌는지 확인할 수 있다.</p>
<h2 id="핵심-2">핵심</h2>
<ul>
<li>레이어 확인</li>
<li>이미지 생성 과정 추적</li>
<li>이미지 최적화 분석</li>
</ul>
<pre><code class="language-bash">docker history myimage</code></pre>
<hr>
<h1 id="4-dockerfile-최적화">4. Dockerfile 최적화</h1>
<p>Docker는 캐시를 최대한 활용하도록 설계되어 있다.</p>
<h2 id="핵심-3">핵심</h2>
<ul>
<li>변경이 적은 작업은 위쪽</li>
<li>변경이 많은 작업은 아래쪽</li>
<li>캐시 효율 증가</li>
<li>빌드 속도 향상</li>
</ul>
<hr>
<h1 id="5-캐시-동작-방식">5. 캐시 동작 방식</h1>
<p>중간 단계에서 캐시가 깨지면 이후 단계도 모두 새로 실행된다.</p>
<h2 id="핵심-4">핵심</h2>
<ul>
<li>특정 레이어 변경</li>
<li>이후 레이어 전부 재빌드</li>
<li>Dockerfile 순서가 중요</li>
</ul>
<hr>
<h1 id="6-캐시-비활성화">6. 캐시 비활성화</h1>
<p>캐시 없이 새롭게 이미지를 빌드할 수 있다.</p>
<h2 id="핵심-5">핵심</h2>
<ul>
<li>모든 단계 새로 실행</li>
<li>테스트 시 유용</li>
</ul>
<pre><code class="language-bash">docker build --no-cache .</code></pre>
<hr>
<h1 id="7-oracle-postgresql-redis-컨테이너">7. Oracle, PostgreSQL, Redis 컨테이너</h1>
<p>Docker로 다양한 데이터베이스를 쉽게 실행할 수 있다.</p>
<h2 id="핵심-6">핵심</h2>
<ul>
<li>Oracle 11g</li>
<li>PostgreSQL</li>
<li>Redis</li>
<li>포트와 볼륨 설정</li>
<li>DBeaver 연동 가능</li>
</ul>
<pre><code class="language-bash">docker run -p 5432:5432 postgres</code></pre>
<hr>
<h1 id="8-redis-특징">8. Redis 특징</h1>
<p>Redis는 메모리 기반 Key-Value 데이터베이스이다.</p>
<h2 id="핵심-7">핵심</h2>
<ul>
<li>캐시 서버</li>
<li>세션 저장</li>
<li>메시지 큐</li>
<li>매우 빠른 조회 속도</li>
</ul>
<hr>
<h1 id="9-java-컴파일과-jar-실행">9. Java 컴파일과 JAR 실행</h1>
<p>컨테이너에서도 Java를 직접 컴파일하고 실행할 수 있다.</p>
<h2 id="핵심-8">핵심</h2>
<ul>
<li>javac 컴파일</li>
<li>jar 생성</li>
<li>java -jar 실행</li>
</ul>
<pre><code class="language-bash">javac Main.java
java Main
java -jar app.jar</code></pre>
<hr>
<h1 id="10-gradle-빌드">10. Gradle 빌드</h1>
<p>Gradle을 이용하면 프로젝트를 자동으로 빌드할 수 있다.</p>
<h2 id="핵심-9">핵심</h2>
<ul>
<li>의존성 관리</li>
<li>JAR 생성</li>
<li>자동 빌드</li>
</ul>
<pre><code class="language-bash">gradle build</code></pre>
<hr>
<h1 id="11-spring-boot-실행">11. Spring Boot 실행</h1>
<p>Gradle 컨테이너에서 Spring 프로젝트를 빌드하고 실행할 수 있다.</p>
<h2 id="핵심-10">핵심</h2>
<ul>
<li>Git Clone</li>
<li>Gradle Build</li>
<li>JAR 실행</li>
<li>포트 매핑</li>
</ul>
<pre><code class="language-bash">java -jar app.jar</code></pre>
<hr>
<h1 id="12-빌드-서버와-운영-서버-분리">12. 빌드 서버와 운영 서버 분리</h1>
<p>빌드는 빌드 서버에서, 실행은 운영 서버에서 수행하는 것이 일반적이다.</p>
<h2 id="핵심-11">핵심</h2>
<ul>
<li>역할 분리</li>
<li>운영 서버 경량화</li>
<li>보안 향상</li>
<li>성능 향상</li>
</ul>
<hr>
<h1 id="13-docker-cp-·-바인드-마운트-·-볼륨">13. docker cp · 바인드 마운트 · 볼륨</h1>
<p>빌드 결과물을 운영 서버로 전달하는 여러 방법을 학습했다.</p>
<h2 id="핵심-12">핵심</h2>
<ul>
<li>docker cp</li>
<li>Bind Mount</li>
<li>Docker Volume</li>
<li>JAR 공유</li>
</ul>
<hr>
<h1 id="14-실행-전용-docker-이미지">14. 실행 전용 Docker 이미지</h1>
<p>빌드된 JAR만 포함하는 이미지를 만들면 어디서든 실행 가능하다.</p>
<h2 id="핵심-13">핵심</h2>
<ul>
<li>GraalVM 기반</li>
<li>JAR 포함</li>
<li>즉시 실행 가능</li>
</ul>
<pre><code class="language-dockerfile">FROM graalvm:21

COPY app.jar /app/app.jar

ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app/app.jar&quot;]</code></pre>
<hr>
<h1 id="15-멀티-스테이지-빌드">15. 멀티 스테이지 빌드</h1>
<p>빌드와 실행 환경을 하나의 Dockerfile에서 관리할 수 있다.</p>
<h2 id="핵심-14">핵심</h2>
<ul>
<li>Build Stage</li>
<li>Runtime Stage</li>
<li>빌드 도구 제거</li>
<li>이미지 크기 감소</li>
</ul>
<pre><code class="language-dockerfile">FROM gradle AS builder

RUN gradle build

FROM graalvm

COPY --from=builder ...</code></pre>
<h3 id="빌드-흐름">빌드 흐름</h3>
<pre><code class="language-text">소스코드
    ↓
Build Stage
    ↓
JAR 생성
    ↓
Runtime Stage
    ↓
최종 이미지</code></pre>
<hr>
<h1 id="16-멀티-스테이지-장점">16. 멀티 스테이지 장점</h1>
<p>복잡했던 배포 과정을 Dockerfile 하나로 단순화할 수 있다.</p>
<h2 id="핵심-15">핵심</h2>
<ul>
<li>docker cp 불필요</li>
<li>별도 빌드 컨테이너 불필요</li>
<li>Dockerfile 하나로 빌드</li>
<li>운영 이미지 최소화</li>
</ul>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>Docker 이미지는 여러 개의 레이어로 구성되며 변경되지 않은 레이어는 캐시를 재사용한다.</li>
<li>Dockerfile은 변경이 적은 명령을 위쪽에 배치할수록 빌드 속도를 높일 수 있다.</li>
<li><code>docker history</code>를 통해 이미지의 레이어 구조를 확인하고 최적화할 수 있다.</li>
<li>Oracle, PostgreSQL, Redis를 Docker 컨테이너로 손쉽게 구축하고 개발 환경에서 활용할 수 있다.</li>
<li>Gradle을 이용해 Spring Boot 프로젝트를 빌드하고 JAR 파일을 생성할 수 있다.</li>
<li>빌드 서버와 운영 서버를 분리하면 보안과 성능 측면에서 유리하다.</li>
<li><code>docker cp</code>, 바인드 마운트, Docker Volume을 이용해 빌드 결과물을 다양한 방식으로 공유할 수 있다.</li>
<li>실행에 필요한 JAR만 포함한 Docker 이미지를 만들면 어디서든 동일한 환경에서 실행할 수 있다.</li>
<li>멀티 스테이지 빌드를 사용하면 빌드와 실행 환경을 하나의 Dockerfile로 관리하면서 이미지 크기와 배포 과정을 모두 최적화할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 69 - Docker]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-69-Docker-1cuvhtfn</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-69-Docker-1cuvhtfn</guid>
            <pubDate>Mon, 06 Jul 2026 00:34:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 06월 30일 작성된 글입니다.</p>
</blockquote>
<p>Docker를 학습하면서 컨테이너의 포트 매핑부터 네트워크 통신, 바인드 마운트, Dockerfile 작성, 이미지 빌드와 레이어 구조까지 학습했다. 특히 컨테이너 간 통신 방식과 Docker Image가 생성되는 과정을 중심으로 정리했다.</p>
<hr>
<h1 id="1-포트-매핑">1. 포트 매핑</h1>
<p>Docker 컨테이너는 외부와 격리되어 실행되므로, 서비스를 외부에서 사용하려면 포트를 연결해야 한다.</p>
<h2 id="핵심">핵심</h2>
<ul>
<li><code>-p</code> : 호스트 포트와 컨테이너 포트를 직접 연결</li>
<li><code>-P</code> : 노출된 포트를 임의의 호스트 포트에 자동 연결</li>
<li>외부에서 서비스에 접근하려면 포트 매핑이 필수이다.</li>
</ul>
<pre><code class="language-bash">docker run -d -p 8080:80 nginx</code></pre>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>-p</code></td>
<td>원하는 포트를 직접 매핑</td>
</tr>
<tr>
<td><code>-P</code></td>
<td>Docker가 임의의 포트를 자동 할당</td>
</tr>
</tbody></table>
<hr>
<h1 id="2-컨테이너-정보-확인">2. 컨테이너 정보 확인</h1>
<p>실행 중인 컨테이너의 설정과 네트워크 정보를 확인할 수 있다.</p>
<h2 id="핵심-1">핵심</h2>
<ul>
<li>컨테이너 ID 확인</li>
<li>IP 주소 확인</li>
<li>포트 매핑 확인</li>
<li>환경 변수 및 실행 정보 확인</li>
</ul>
<pre><code class="language-bash">docker inspect mynginx</code></pre>
<p>특정 정보만 확인할 수도 있다.</p>
<pre><code class="language-bash">docker inspect mynginx | grep &#39;&quot;IPAddress&quot;&#39;</code></pre>
<hr>
<h1 id="3-컨테이너-내부-작업">3. 컨테이너 내부 작업</h1>
<p>실행 중인 컨테이너에 접속하거나 파일을 복사할 수 있다.</p>
<h2 id="핵심-2">핵심</h2>
<ul>
<li><code>docker exec</code> : 컨테이너 내부 명령 실행</li>
<li><code>docker cp</code> : 호스트 ↔ 컨테이너 파일 복사</li>
<li>웹루트에 HTML을 추가하여 즉시 서비스 가능</li>
</ul>
<pre><code class="language-bash">docker exec -it mynginx bash</code></pre>
<pre><code class="language-bash">docker cp index.html mynginx:/usr/share/nginx/html/</code></pre>
<hr>
<h1 id="4-바인드-마운트">4. 바인드 마운트</h1>
<p>호스트 디렉터리와 컨테이너 디렉터리를 연결하는 기능이다.</p>
<h2 id="핵심-3">핵심</h2>
<ul>
<li>호스트에서 수정하면 즉시 반영</li>
<li>컨테이너 삭제 후에도 파일 유지</li>
<li>개발 환경에서 매우 유용</li>
</ul>
<pre><code class="language-bash">docker run \
-v ./html:/usr/share/nginx/html</code></pre>
<h3 id="장점">장점</h3>
<ul>
<li>실시간 반영</li>
<li>파일 유지</li>
<li>코드 관리 편리</li>
</ul>
<hr>
<h1 id="5-컨테이너-네트워크">5. 컨테이너 네트워크</h1>
<p>컨테이너끼리 다양한 방법으로 통신할 수 있다.</p>
<h2 id="핵심-4">핵심</h2>
<ul>
<li>기본 Bridge Network 제공</li>
<li>사용자 정의 Network 생성 가능</li>
<li>IP 또는 컨테이너 이름으로 통신 가능</li>
</ul>
<pre><code class="language-bash">docker network create common</code></pre>
<h3 id="통신-방식">통신 방식</h3>
<table>
<thead>
<tr>
<th>방식</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>Bridge</td>
<td>기본 네트워크</td>
</tr>
<tr>
<td>User Network</td>
<td>컨테이너 이름(DNS) 지원</td>
</tr>
<tr>
<td>Host Port</td>
<td>호스트 포트를 통해 통신</td>
</tr>
<tr>
<td>host.docker.internal</td>
<td>호스트 OS 접근</td>
</tr>
</tbody></table>
<hr>
<h1 id="6-docker-exec와-docker-cp-활용">6. docker exec와 docker cp 활용</h1>
<p>컨테이너 내부를 직접 수정하지 않아도 필요한 파일을 쉽게 관리할 수 있다.</p>
<h2 id="핵심-5">핵심</h2>
<ul>
<li>컨테이너 접속 없이 파일 복사 가능</li>
<li>호스트와 컨테이너 간 양방향 복사 지원</li>
<li>웹 리소스 관리에 자주 사용</li>
</ul>
<pre><code class="language-bash">docker cp sub.html mynginx:/usr/share/nginx/html/</code></pre>
<hr>
<h1 id="7-docker-image와-dockerfile">7. Docker Image와 Dockerfile</h1>
<p>Docker 이미지는 애플리케이션뿐 아니라 실행 환경까지 함께 포함하는 패키지이다.</p>
<h2 id="핵심-6">핵심</h2>
<ul>
<li>실행 환경까지 함께 배포</li>
<li>Dockerfile을 기반으로 이미지 생성</li>
<li>동일한 환경을 어디서나 재현 가능</li>
</ul>
<pre><code class="language-dockerfile">FROM nginx:latest

COPY source/index.html /usr/share/nginx/html/</code></pre>
<p>이미지 생성</p>
<pre><code class="language-bash">docker build -t myname/nginx:1 .</code></pre>
<hr>
<h1 id="8-docker-image-layer">8. Docker Image Layer</h1>
<p>Docker 이미지는 여러 개의 Layer로 구성된다.</p>
<h2 id="핵심-7">핵심</h2>
<ul>
<li>모든 Layer는 Immutable(불변)</li>
<li>Dockerfile 한 줄마다 Layer 생성</li>
<li>변경되지 않은 Layer는 재사용</li>
</ul>
<pre><code class="language-dockerfile">FROM nginx:latest
COPY source/index.html /usr/share/nginx/html/
RUN apt-get update</code></pre>
<h3 id="주요-layer">주요 Layer</h3>
<table>
<thead>
<tr>
<th>명령어</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>FROM</td>
<td>기본 이미지</td>
</tr>
<tr>
<td>COPY</td>
<td>파일 복사</td>
</tr>
<tr>
<td>RUN</td>
<td>명령 실행</td>
</tr>
</tbody></table>
<hr>
<h1 id="9-docker-cache">9. Docker Cache</h1>
<p>Docker는 변경되지 않은 Layer를 캐시하여 빌드 시간을 줄인다.</p>
<h2 id="핵심-8">핵심</h2>
<ul>
<li>변경된 Layer부터 다시 빌드</li>
<li>이전 Layer는 그대로 재사용</li>
<li>Dockerfile 작성 순서가 중요</li>
</ul>
<h3 id="예시">예시</h3>
<pre><code class="language-text">FROM
 ↓
COPY
 ↓
RUN</code></pre>
<ul>
<li><code>COPY</code>가 변경되면 이후 <code>RUN</code>도 다시 실행된다.</li>
<li>아무것도 변경되지 않았다면 모든 Layer가 캐시를 사용한다.</li>
</ul>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>Docker 컨테이너는 포트 매핑을 통해 외부와 통신할 수 있다.</li>
<li><code>docker inspect</code>, <code>docker exec</code>, <code>docker cp</code>를 이용하면 실행 중인 컨테이너를 쉽게 관리할 수 있다.</li>
<li>바인드 마운트를 사용하면 호스트와 컨테이너가 동일한 파일을 공유하여 개발이 편리해진다.</li>
<li>사용자 정의 네트워크를 사용하면 컨테이너 이름만으로도 통신할 수 있다.</li>
<li><code>host.docker.internal</code>을 이용하면 컨테이너에서 호스트 OS의 서비스에도 접근할 수 있다.</li>
<li>Docker Image는 애플리케이션뿐 아니라 실행 환경까지 함께 포함하여 동일한 실행 환경을 보장한다.</li>
<li>Dockerfile의 각 명령은 하나의 Layer를 생성하며, Layer 구조 덕분에 캐시를 효율적으로 활용할 수 있다.</li>
<li>Docker의 네트워크와 이미지 구조를 이해하면 개발과 배포 환경을 더욱 안정적으로 관리할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 68 - Bash Shell pt.2]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-68-Bash-Shell-pt.2</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-68-Bash-Shell-pt.2</guid>
            <pubDate>Thu, 02 Jul 2026 01:35:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2025년 06월 29일 작성된 글입니다.</p>
</blockquote>
<p>백엔드 개발에서는 리눅스 환경을 자주 사용하기 때문에 Bash Shell 명령어를
익혀두는 것이 중요하다. 이번에는 파일과 디렉토리 관리, VIM, 파이프, 이동
및 복사 등 Bash Shell의 기본 명령어와 실습 내용을 정리했다.</p>
<hr>
<h1 id="1-bash-shell이란">1. Bash Shell이란?</h1>
<p>Bash(Bourne Again SHell)는 리눅스와 macOS에서 기본적으로 사용하는 명령어
인터프리터(Shell)이다.</p>
<h2 id="핵심">핵심</h2>
<ul>
<li>터미널에서 명령어를 실행하는 프로그램</li>
<li>리눅스 및 macOS 기본 Shell</li>
<li>파일 및 디렉토리 관리</li>
<li>프로그램 실행 및 시스템 관리</li>
</ul>
<hr>
<h1 id="2-파일-및-디렉토리-조회">2. 파일 및 디렉토리 조회</h1>
<h2 id="핵심-1">핵심</h2>
<ul>
<li><p>현재 위치 확인</p>
</li>
<li><p>파일 목록 조회</p>
</li>
<li><p>숨김 파일 확인</p>
</li>
<li><p>화면 초기화</p>
<p>명령어     설명</p>
<hr>
<p><code>pwd</code>      현재 위치 출력
<code>ls</code>       파일 목록 출력
<code>ls -l</code>    상세 목록 출력
<code>ll</code>       <code>ls -l</code> 축약형
<code>ls -al</code>   숨김 파일까지 출력
<code>clear</code>    화면 지우기</p>
</li>
</ul>
<pre><code class="language-bash">pwd
ls
ls -al
clear</code></pre>
<hr>
<h1 id="3-디렉토리-이동">3. 디렉토리 이동</h1>
<h2 id="핵심-2">핵심</h2>
<ul>
<li>홈 디렉토리 이동</li>
<li>절대경로 이동</li>
<li>상대경로 이동</li>
<li>상위 디렉토리 이동</li>
</ul>
<pre><code class="language-bash">cd ~
cd /폴더명
cd ./폴더명
cd ..
cd .</code></pre>
<hr>
<h1 id="4-파일과-디렉토리-생성">4. 파일과 디렉토리 생성</h1>
<h2 id="핵심-3">핵심</h2>
<ul>
<li>파일 생성</li>
<li>디렉토리 생성</li>
<li>여러 디렉토리 생성</li>
</ul>
<pre><code class="language-bash">touch memo.txt
mkdir test
mkdir -p a/b/c/d</code></pre>
<hr>
<h1 id="5-파일-및-디렉토리-삭제">5. 파일 및 디렉토리 삭제</h1>
<h2 id="핵심-4">핵심</h2>
<ul>
<li>파일 삭제</li>
<li>빈 디렉토리 삭제</li>
<li>강제 삭제</li>
<li>와일드카드 사용</li>
</ul>
<pre><code class="language-bash">rm memo.txt
rm *
rmdir test
rm -rf test</code></pre>
<h3 id="숨김-파일">숨김 파일</h3>
<pre><code class="language-bash">touch .hidden
ls -al
rm .hidden</code></pre>
<hr>
<h1 id="6-echo와-리다이렉션">6. echo와 리다이렉션</h1>
<h2 id="핵심-5">핵심</h2>
<ul>
<li>화면 출력</li>
<li>덮어쓰기</li>
<li>이어쓰기</li>
</ul>
<pre><code class="language-bash">echo &quot;Hello&quot;
echo &quot;Hello&quot; &gt; test.txt
echo &quot;World&quot; &gt;&gt; test.txt
cat test.txt</code></pre>
<p>  기호   의미</p>
<hr>
<p>  <code>&gt;</code>    덮어쓰기
  <code>&gt;&gt;</code>   이어쓰기</p>
<hr>
<h1 id="7-vim">7. VIM</h1>
<h2 id="핵심-6">핵심</h2>
<ul>
<li><p>입력 모드</p>
</li>
<li><p>명령 모드</p>
</li>
<li><p>저장</p>
</li>
<li><p>저장 없이 종료</p>
<p>명령    설명</p>
<hr>
<p><code>a</code>     입력 모드
<code>ESC</code>   명령 모드
<code>:wq</code>   저장 후 종료
<code>:q!</code>   저장 없이 종료</p>
</li>
</ul>
<hr>
<h1 id="8-파이프">8. 파이프</h1>
<h2 id="핵심-7">핵심</h2>
<ul>
<li>명령어 연결</li>
<li>원하는 결과만 추출</li>
<li>파일 저장</li>
</ul>
<pre><code class="language-bash">history | fgrep ls
ps -aux | fgrep sshd
history | fgrep mkdir &gt; mk_history.txt</code></pre>
<hr>
<h1 id="9-파일-이동-및-복사">9. 파일 이동 및 복사</h1>
<h2 id="핵심-8">핵심</h2>
<ul>
<li>파일 이동</li>
<li>이름 변경</li>
<li>파일 복사</li>
<li>디렉토리 복사</li>
</ul>
<pre><code class="language-bash">mv aaa folder
cp aaa bbb
cp -r folder backup</code></pre>
<hr>
<h1 id="10-압축">10. 압축</h1>
<h2 id="핵심-9">핵심</h2>
<ul>
<li>압축</li>
<li>압축 해제</li>
</ul>
<pre><code class="language-bash">zip file.zip a.txt b.txt
unzip file.zip</code></pre>
<hr>
<h1 id="11-자주-사용하는-명령어">11. 자주 사용하는 명령어</h1>
<p>  기능          명령어</p>
<hr>
<p>  현재 위치     <code>pwd</code>
  파일 조회     <code>ls</code>, <code>ls -al</code>
  이동          <code>cd</code>
  파일 생성     <code>touch</code>
  폴더 생성     <code>mkdir</code>
  삭제          <code>rm</code>, <code>rm -rf</code>
  내용 출력     <code>cat</code>
  문자열 출력   <code>echo</code>
  편집          <code>vim</code>
  이동          <code>mv</code>
  복사          <code>cp</code>
  압축          <code>zip</code></p>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>Bash Shell은 운영체제와 상호작용하기 위한 기본 Shell 프로그램이다.</li>
<li><code>ls</code>, <code>cd</code>, <code>pwd</code>를 이용하여 파일과 디렉토리를 조회하고 이동할 수
있다.</li>
<li><code>touch</code>, <code>mkdir</code>, <code>rm</code>, <code>rm -rf</code>를 이용하여 파일과 디렉토리를
생성하거나 삭제할 수 있다.</li>
<li><code>echo</code>, <code>cat</code>, 리다이렉션을 이용하여 파일 내용을 관리할 수 있다.</li>
<li>VIM에서는 입력 모드와 명령 모드를 전환하여 파일을 편집할 수 있다.</li>
<li>파이프(<code>|</code>)를 사용하면 여러 명령어를 연결하여 원하는 결과만 추출할
수 있다.</li>
<li><code>mv</code>와 <code>cp</code>를 이용하여 파일과 디렉토리를 이동하거나 복사할 수 있다.</li>
<li>Bash 기본 명령어는 백엔드 개발과 리눅스 서버 운영의 기초이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 67 - Bash Shell]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-67-Bash-Shell</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-67-Bash-Shell</guid>
            <pubDate>Sat, 27 Jun 2026 05:23:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 06월 26일 작성된 글입니다.</p>
</blockquote>
<p>Bash Shell은 리눅스와 macOS에서 사용하는 기본 명령어 인터프리터이다. 백엔드 개발에서는 서버 접속, 파일 관리, Git 사용 등 대부분의 작업을 터미널에서 수행하기 때문에 Bash 명령어를 익혀두는 것이 중요하다.</p>
<hr>
<h1 id="1-bash-shell이란">1. Bash Shell이란?</h1>
<p>Bash(Bourne Again SHell)는 사용자가 입력한 명령어를 운영체제가 이해할 수 있도록 전달하는 명령어 인터프리터(Shell)이다.</p>
<p>터미널을 통해 파일 관리, 프로그램 실행, 서버 관리 등 다양한 작업을 수행할 수 있다.</p>
<h2 id="핵심">핵심</h2>
<ul>
<li>운영체제와 사용자를 연결하는 CLI(Command Line Interface) 환경이다.</li>
<li>리눅스와 macOS에서 기본적으로 사용되는 Shell이다.</li>
<li>백엔드 개발과 서버 운영에서 가장 많이 사용하는 도구이다.</li>
</ul>
<pre><code class="language-bash">pwd
ls
cd</code></pre>
<hr>
<h1 id="2-파일-및-디렉토리-조회">2. 파일 및 디렉토리 조회</h1>
<p>현재 위치를 확인하거나 파일 및 디렉토리 목록을 조회할 때 사용하는 명령어이다.</p>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>pwd</code></td>
<td>현재 위치 출력</td>
</tr>
<tr>
<td><code>ls</code></td>
<td>파일 목록 조회</td>
</tr>
<tr>
<td><code>ls -l</code></td>
<td>상세 정보 조회</td>
</tr>
<tr>
<td><code>ls -al</code></td>
<td>숨김 파일 포함 조회</td>
</tr>
<tr>
<td><code>clear</code></td>
<td>화면 지우기</td>
</tr>
</tbody></table>
<h2 id="핵심-1">핵심</h2>
<ul>
<li><code>pwd</code>는 현재 작업 중인 디렉토리를 확인한다.</li>
<li><code>ls -al</code>은 숨김 파일까지 모두 출력한다.</li>
</ul>
<pre><code class="language-bash">pwd
ls
ls -al
clear</code></pre>
<hr>
<h1 id="3-디렉토리-이동">3. 디렉토리 이동</h1>
<p>디렉토리를 이동할 때 사용하는 명령어이다.</p>
<h2 id="핵심-2">핵심</h2>
<ul>
<li><code>cd ~</code>는 홈 디렉토리로 이동한다.</li>
<li><code>cd ..</code>는 상위 디렉토리로 이동한다.</li>
<li>절대경로와 상대경로를 모두 사용할 수 있다.</li>
</ul>
<pre><code class="language-bash">cd ~
cd ..
cd project
cd /Users/minhyuk/project</code></pre>
<h3 id="절대경로와-상대경로">절대경로와 상대경로</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>절대경로</td>
<td>루트(<code>/</code>)부터 시작하는 경로</td>
</tr>
<tr>
<td>상대경로</td>
<td>현재 위치를 기준으로 하는 경로</td>
</tr>
</tbody></table>
<hr>
<h1 id="4-파일-및-디렉토리-생성">4. 파일 및 디렉토리 생성</h1>
<p>파일과 디렉토리를 생성할 때 사용하는 명령어이다.</p>
<h2 id="핵심-3">핵심</h2>
<ul>
<li><code>touch</code>는 빈 파일을 생성한다.</li>
<li><code>mkdir</code>는 디렉토리를 생성한다.</li>
<li><code>mkdir -p</code>는 중간 경로까지 한 번에 생성한다.</li>
</ul>
<pre><code class="language-bash">touch test.txt

mkdir project

mkdir -p a/b/c</code></pre>
<hr>
<h1 id="5-파일-및-디렉토리-삭제">5. 파일 및 디렉토리 삭제</h1>
<p>파일과 디렉토리를 삭제할 때 사용하는 명령어이다.</p>
<h2 id="핵심-4">핵심</h2>
<ul>
<li><code>rm</code>은 파일을 삭제한다.</li>
<li><code>rmdir</code>은 비어있는 디렉토리만 삭제할 수 있다.</li>
<li><code>rm -rf</code>는 디렉토리를 강제로 삭제한다.</li>
</ul>
<pre><code class="language-bash">rm test.txt

rmdir project

rm -rf project</code></pre>
<h3 id="삭제-명령어-비교">삭제 명령어 비교</h3>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>rm</code></td>
<td>파일 삭제</td>
</tr>
<tr>
<td><code>rmdir</code></td>
<td>빈 디렉토리 삭제</td>
</tr>
<tr>
<td><code>rm -rf</code></td>
<td>디렉토리 및 내부 파일까지 강제 삭제</td>
</tr>
</tbody></table>
<hr>
<h1 id="6-echo와-리다이렉션">6. echo와 리다이렉션</h1>
<p><code>echo</code>는 문자열을 출력하거나 파일에 저장할 때 사용한다.</p>
<h2 id="핵심-5">핵심</h2>
<ul>
<li><code>&gt;</code>는 기존 내용을 덮어쓴다.</li>
<li><code>&gt;&gt;</code>는 기존 내용 뒤에 이어서 작성한다.</li>
</ul>
<pre><code class="language-bash">echo &quot;Hello&quot;

echo &quot;Hello&quot; &gt; test.txt

echo &quot;World&quot; &gt;&gt; test.txt</code></pre>
<h3 id="리다이렉션">리다이렉션</h3>
<table>
<thead>
<tr>
<th>기호</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>&gt;</code></td>
<td>덮어쓰기</td>
</tr>
<tr>
<td><code>&gt;&gt;</code></td>
<td>이어쓰기</td>
</tr>
</tbody></table>
<hr>
<h1 id="7-cat-명령어">7. cat 명령어</h1>
<p>파일의 내용을 출력할 때 사용하는 명령어이다.</p>
<h2 id="핵심-6">핵심</h2>
<ul>
<li>파일 내용을 빠르게 확인할 수 있다.</li>
<li>텍스트 파일 확인 시 가장 많이 사용하는 명령어이다.</li>
</ul>
<pre><code class="language-bash">cat test.txt</code></pre>
<hr>
<h1 id="8-vim-편집기">8. VIM 편집기</h1>
<p>VIM은 터미널 환경에서 사용하는 대표적인 텍스트 편집기이다.</p>
<h2 id="핵심-7">핵심</h2>
<ul>
<li>입력 모드와 명령 모드를 구분하여 사용한다.</li>
<li>저장과 종료 명령을 익혀두면 대부분의 작업이 가능하다.</li>
</ul>
<pre><code class="language-text">a      입력 모드

ESC    명령 모드

:wq    저장 후 종료

:q!    저장하지 않고 종료</code></pre>
<hr>
<h1 id="9-pipe와-fgrep">9. Pipe와 fgrep</h1>
<p>Pipe(<code>|</code>)는 앞 명령어의 실행 결과를 다음 명령어의 입력으로 전달한다.</p>
<p><code>fgrep</code>은 특정 문자열이 포함된 결과만 필터링할 때 사용한다.</p>
<h2 id="핵심-8">핵심</h2>
<ul>
<li>여러 명령어를 연결하여 사용할 수 있다.</li>
<li>원하는 결과만 추출할 수 있다.</li>
</ul>
<pre><code class="language-bash">history | fgrep mkdir

ps -aux | fgrep sshd

ls -al | less</code></pre>
<h3 id="동작-흐름">동작 흐름</h3>
<pre><code class="language-text">명령어 실행
      ↓
Pipe(|)
      ↓
fgrep / less
      ↓
필요한 결과만 출력</code></pre>
<hr>
<h1 id="10-파일-이동-및-복사">10. 파일 이동 및 복사</h1>
<p>파일이나 디렉토리를 이동하거나 복사할 때 사용하는 명령어이다.</p>
<h2 id="핵심-9">핵심</h2>
<ul>
<li><code>mv</code>는 이동과 이름 변경을 모두 수행한다.</li>
<li>디렉토리 복사는 <code>cp -r</code> 옵션이 필요하다.</li>
</ul>
<pre><code class="language-bash">mv aaa bbb

cp aaa ccc

cp -r folder1 folder2</code></pre>
<hr>
<h1 id="11-zip-압축">11. ZIP 압축</h1>
<p>파일을 압축하거나 압축을 해제할 때 사용하는 명령어이다.</p>
<h2 id="핵심-10">핵심</h2>
<ul>
<li>여러 파일을 하나의 ZIP 파일로 압축할 수 있다.</li>
<li><code>unzip</code>으로 쉽게 압축을 해제할 수 있다.</li>
</ul>
<pre><code class="language-bash">zip files.zip a.txt b.txt

unzip files.zip</code></pre>
<hr>
<h1 id="12-파일-권한permission">12. 파일 권한(Permission)</h1>
<p>리눅스에서는 파일과 디렉토리에 읽기(Read), 쓰기(Write), 실행(Execute) 권한이 존재한다.</p>
<h2 id="핵심-11">핵심</h2>
<ul>
<li><code>r</code>, <code>w</code>, <code>x</code> 권한으로 구성된다.</li>
<li><code>chmod</code>를 이용해 권한을 변경할 수 있다.</li>
</ul>
<pre><code class="language-bash">chmod 755 run.sh</code></pre>
<h3 id="권한-숫자">권한 숫자</h3>
<table>
<thead>
<tr>
<th>권한</th>
<th>숫자</th>
</tr>
</thead>
<tbody><tr>
<td>rwx</td>
<td>7</td>
</tr>
<tr>
<td>rw-</td>
<td>6</td>
</tr>
<tr>
<td>r-x</td>
<td>5</td>
</tr>
<tr>
<td>r--</td>
<td>4</td>
</tr>
</tbody></table>
<h3 id="권한-예시">권한 예시</h3>
<pre><code class="language-text">-rwxr-xr-x</code></pre>
<ul>
<li>첫 글자가 <code>d</code>이면 디렉토리이다.</li>
<li>첫 글자가 <code>-</code>이면 일반 파일이다.</li>
</ul>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>Bash Shell은 운영체제와 사용자가 명령어로 상호작용할 수 있도록 도와주는 명령어 인터프리터이다.</li>
<li><code>pwd</code>, <code>ls</code>, <code>cd</code>는 파일과 디렉토리를 탐색할 때 가장 기본이 되는 명령어이다.</li>
<li><code>touch</code>, <code>mkdir</code>, <code>rm</code>, <code>rm -rf</code>를 이용해 파일과 디렉토리를 생성하고 삭제할 수 있다.</li>
<li><code>echo</code>, <code>cat</code>, 리다이렉션을 이용하면 파일을 생성하고 내용을 저장하거나 확인할 수 있다.</li>
<li>VIM은 터미널 환경에서 자주 사용하는 텍스트 편집기로 입력 모드와 명령 모드를 구분해서 사용한다.</li>
<li>Pipe와 <code>fgrep</code>을 활용하면 여러 명령어를 연결하여 원하는 결과만 효율적으로 추출할 수 있다.</li>
<li><code>mv</code>, <code>cp</code>, <code>zip</code>, <code>chmod</code>는 파일 관리와 서버 작업에서 자주 사용하는 핵심 명령어이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Project - Wedge 💍]]></title>
            <link>https://velog.io/@xxxhyxxk__/Project-Wedge</link>
            <guid>https://velog.io/@xxxhyxxk__/Project-Wedge</guid>
            <pubDate>Fri, 26 Jun 2026 00:18:12 GMT</pubDate>
            <description><![CDATA[<h1 id="💍-wedge-프로젝트-회고--2주-만에-웨딩-매칭-플랫폼-만들기">💍 Wedge 프로젝트 회고 — 2주 만에 웨딩 매칭 플랫폼 만들기</h1>
<blockquote>
<p><strong>2026.06.11 ~ 06.24 | 팀 프로젝트 | 4인 개발</strong></p>
</blockquote>
<hr>
<h2 id="프로젝트-소개">프로젝트 소개</h2>
<p><strong>Wedge</strong>는 예비부부(CLIENT)와 웨딩 전문 프리랜서(FREELANCER)를 연결하는 매칭 플랫폼이다.</p>
<p>웨딩홀 패키지에 묶여 있으면 비싸고 선택지도 좁다. 반대로 프리랜서 입장에서는 실력이 있어도 홍보 채널이 없으면 일감을 못 찾는다. Wedge는 이 둘을 직접 연결하는 구조다. 핵심 차별점은 <strong>역경매 방식</strong>: 예비부부가 구인글을 올리면 프리랜서가 직접 제안서를 내고, 부부가 마음에 드는 제안을 수락하면 예약이 자동 생성된다.</p>
<p>기능 범위가 꽤 넓었다. 인증, 프로필, 포트폴리오, 북마크, 예약, 실시간 채팅, 리뷰, 커뮤니티, 구인/제안, AI 챗봇, CI/CD까지 — 이걸 2주 안에 4명이 완성해야 했다.</p>
<p>배포 URL: <a href="https://wedge-tawny.vercel.app">wedge-tawny.vercel.app</a><br>시연 영상: <a href="https://streamable.com/6dklaq">streamable.com/6dklaq</a></p>
<hr>
<h2 id="팀원과-역할-분담">팀원과 역할 분담</h2>
<table>
<thead>
<tr>
<th>이름</th>
<th>담당</th>
</tr>
</thead>
<tbody><tr>
<td><strong>윤하빈</strong></td>
<td>찜(북마크), 전문가 탐색 API, 추천 캐러셀(Redis 캐싱), AI 견적 챗봇</td>
</tr>
<tr>
<td><strong>정민혁</strong> (나)</td>
<td>회원 인증(JWT·OAuth2·이메일), 구인/제안 시스템, 커뮤니티, CI/CD, AWS 배포</td>
</tr>
<tr>
<td><strong>이창민</strong></td>
<td>예약 시스템, 실시간 채팅(WebSocket·STOMP), 리뷰 기능</td>
</tr>
<tr>
<td><strong>이민서</strong></td>
<td>프리랜서 프로필, 포트폴리오(Cloudflare R2), 마이페이지 UX, 디자인 시스템</td>
</tr>
</tbody></table>
<hr>
<h2 id="내가-맡은-파트--기술적-결정들">내가 맡은 파트 — 기술적 결정들</h2>
<h3 id="1-인증-시스템-jwt--rtr--oauth2">1. 인증 시스템: JWT + RTR + OAuth2</h3>
<p>인증은 대부분의 기능이 의존하는 뿌리라서 가장 먼저, 가장 꼼꼼하게 설계했다.</p>
<p><strong>RTR(Refresh Token Rotation)</strong></p>
<p>일반 JWT 구조에서 Refresh Token이 탈취되면, 만료될 때까지 공격자가 계속 쓸 수 있다. RTR을 적용하면 토큰을 사용할 때마다 새 토큰으로 교체되므로, 탈취 피해를 최소화할 수 있다.</p>
<p>구현 방식은 간단하다. Redis에 <code>memberId</code>를 키로 Refresh Token 1개만 유지한다. <code>save()</code>를 호출하면 기존 토큰이 덮어써지면서 자동으로 무효화된다. 누가 먼저 쓰든 나머지 한쪽은 차단된다.</p>
<pre><code class="language-java">refreshTokenRepository.save(RefreshToken.builder()
    .memberId(memberId)
    .token(newRefreshToken)  // upsert — 기존 토큰 자동 무효화
    .build());</code></pre>
<p><strong>이메일 인증 상태 머신</strong></p>
<p>Redis 키를 두 개로 분리했다: 인증 코드(TTL 5분)와 완료 플래그(TTL 30분). 역할과 만료 시간이 다르기 때문에 하나로 합칠 수 없다. 인증 코드 검증 즉시 삭제해서 1회용을 보장하고, 회원가입 완료 시 플래그도 삭제해서 중복 가입을 차단한다. DB 대신 Redis를 쓴 이유는 TTL 덕에 만료 배치 작업이 필요 없고, 조회도 ~1ms로 빠르기 때문이다.</p>
<p><strong>OAuth2 (Google + Kakao) 통합</strong></p>
<p>구글과 카카오는 JSON 응답 구조가 다르다. providerId, email, name의 경로가 제각각이다. <code>OAuth2MemberService.loadUser()</code>에서 provider별 파싱만 다르게 처리하고, 그 이후는 동일한 Member 엔티티로 통합했다. 프론트엔드는 로그인 경로와 상관없이 JWT 하나로만 통신한다.</p>
<hr>
<h3 id="2-mixed-content--caddy-리버스-프록시">2. Mixed Content &amp; Caddy 리버스 프록시</h3>
<p>배포 직전에 생각지 못한 문제가 터졌다. Vercel(HTTPS)에서 AWS EC2(HTTP, 8080) API를 호출하니 브라우저가 <strong>Mixed Content</strong> 정책으로 전부 차단했다.</p>
<pre><code>브라우저 → Vercel(HTTPS) → AWS EC2:8080(HTTP) ← 차단!</code></pre><p>해결책은 Caddy였다.</p>
<pre><code>api.wedge.o-r.kr {
    reverse_proxy localhost:8080
}</code></pre><p>이 두 줄이 전부다. 도메인을 적으면 Caddy가 Let&#39;s Encrypt에서 인증서를 자동 발급하고, 만료 30일 전에 알아서 갱신한다.</p>
<pre><code>브라우저 → Vercel(HTTPS) → Caddy(HTTPS, 443) → Spring Boot(HTTP, 8080)</code></pre><p>Spring Boot에 SSL을 직접 붙이면 keystore 설정, 인증서 갱신 시 재시작이 필요한데, Caddy로 분리하면 그 복잡함이 사라진다. 2주짜리 프로젝트에서 인프라에 쓸 수 있는 시간은 제한적이었고, Caddy 덕분에 인프라 세팅 시간을 크게 줄일 수 있었다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Caddy</th>
<th>Nginx</th>
</tr>
</thead>
<tbody><tr>
<td>SSL 인증서</td>
<td>자동 발급 + 자동 갱신</td>
<td>certbot 별도 설치, cron 등록</td>
</tr>
<tr>
<td>설정 파일</td>
<td>2줄</td>
<td>20줄+</td>
</tr>
</tbody></table>
<hr>
<h3 id="3-cicd-파이프라인">3. CI/CD 파이프라인</h3>
<p><code>dev</code> 브랜치에 push되면 CI가 돌고, <code>main</code>에 머지되면 자동 배포된다.</p>
<p><strong>CI</strong>: GitHub Actions가 MySQL 8.0 + Redis 서비스 컨테이너를 직접 띄워서 통합 테스트를 돌린다. Mock이 아니라 실제 DB에 쿼리를 날린다. 테스트가 하나라도 실패하면 PR 머지가 차단된다.</p>
<p><strong>CD</strong>: <code>bootJar</code> 빌드 → Docker 이미지 빌드 → Docker Hub 푸시(latest + commit SHA 태그) → SSH로 EC2 접속 → 컨테이너 교체.</p>
<p>EC2에서 직접 Gradle 빌드하면 t2.micro(메모리 1GB)가 OOM으로 죽는다. 그래서 GitHub Actions에서 빌드해서 완성된 이미지만 EC2로 당겨오는 구조를 택했다. commit SHA 태그를 따로 붙여두는 이유는 롤백을 위해서다. <code>latest</code>만 있으면 이전 버전으로 돌아가기 어렵다.</p>
<p><strong>Docker 멀티 스테이지 빌드</strong>도 적용했다. Stage 1(JDK + Gradle)에서 빌드하고, Stage 2(JRE만)에서 jar를 복사해 실행한다. 최종 이미지에 JDK와 소스코드가 들어가지 않아서 이미지가 ~300MB 가벼워진다.</p>
<p>배포 흐름 전체:</p>
<pre><code>조직 레포 dev → main 머지
    ↓ (포크 자동 동기화)
개인 포크 main 업데이트
    ↓ (CD 트리거)
GitHub Actions: 빌드 → Docker Hub
    ↓ (SSH)
AWS EC2: pull → 컨테이너 교체 → 완료</code></pre><hr>
<h3 id="4-구인제안-시스템">4. 구인/제안 시스템</h3>
<p>구인글에서 제안서 수락까지 이어지는 흐름을 설계했다. 제안서가 수락되면 예약이 <strong>자동 생성</strong>된다. DB 제약도 꼼꼼하게 걸었다: 구인글 동시 등록 최대 3건, 제안서 동시 제출 최대 5건. 마감된 구인글은 수정 차단, <code>proposals</code> 테이블에 <code>(recruit_post_id, freelancer_profile_id)</code> 복합 UNIQUE 키로 중복 제안을 막았다.</p>
<hr>
<h3 id="5-커뮤니티-게시판">5. 커뮤니티 게시판</h3>
<p>REVIEW/TIP 카테고리로 분리된 커뮤니티 게시판을 구현했다. 소프트 삭제(<code>is_deleted</code>)를 적용해서 데이터는 남기고 노출만 끄는 구조다. 후반에 이미지 첨부 기능과 프리랜서 언급 기능까지 추가됐다.</p>
<hr>
<h2 id="팀-협업-잘-된-것들">팀 협업: 잘 된 것들</h2>
<p><strong>Git 전략이 단순하고 명확했다.</strong> <code>dev</code> 브랜치에서 기능 브랜치를 따고, PR을 통해 합쳤다. CI가 붙어 있으니 깨진 코드가 dev에 섞이는 일이 거의 없었다.</p>
<p><strong>역할 분리가 깔끔했다.</strong> 백엔드 4명이 도메인 단위로 나눠서 작업했는데, 인터페이스(API 스펙)만 맞추면 서로 크게 충돌하지 않았다. 프론트는 이민서 님이 디자인 시스템과 마이페이지를 담당하면서 UI 일관성을 잡아줬다.</p>
<p><strong>Mock 없이 통합 테스트.</strong> CI에서 실제 MySQL과 Redis를 띄워서 테스트했기 때문에 &quot;로컬에서는 됐는데 서버에서 안 됨&quot; 같은 상황이 훨씬 줄었다.</p>
<hr>
<h2 id="아쉬운-것들">아쉬운 것들</h2>
<p><strong>t2.micro의 한계.</strong> 메모리 1GB로 MySQL + Redis + Spring Boot를 동시에 돌리니 가끔 OOM이 났다. Swap 2GB를 추가해서 겨우 버텼는데, 실제 서비스라면 인스턴스 스펙을 올려야 한다.</p>
<p><strong>테스트 커버리지.</strong> CI에서 통합 테스트를 돌리긴 했지만, 작성된 테스트의 양이 충분하지 않았다. 빠르게 기능을 만드는 데 집중하다 보니 엣지 케이스 테스트가 부족했다.</p>
<p><strong>AI 기능과 채팅이 후순위로 밀렸다.</strong> 원래 MVP 우선순위에서 하위였던 기능들이 마지막에 붙었다. 완성은 됐지만 더 다듬을 시간이 없었다.</p>
<p><strong>배포 자동화의 빈틈.</strong> 조직 레포 → 개인 포크 동기화를 GitHub Actions로 자동화했는데, 간혹 sync가 늦거나 실패하는 케이스가 있었다. 더 견고한 파이프라인이 필요하다.</p>
<hr>
<h2 id="배운-것">배운 것</h2>
<p>2주는 생각보다 훨씬 짧다. 기능을 처음부터 완벽하게 만들려고 하면 아무것도 완성되지 않는다. MVP 우선순위를 정하고, 핵심이 아닌 것은 과감하게 뒤로 미루는 판단이 중요했다.</p>
<p>인증 시스템은 프로젝트 초반에 완성도 있게 설계해두길 잘했다. 모든 기능이 인증에 의존하기 때문에, 이게 불안정하면 팀 전체가 흔들린다.</p>
<p>Caddy는 정말 좋은 도구다. 단, ACME 챌린지를 위해 80포트가 열려 있어야 한다는 걸 처음에 몰라서 삽질했다.</p>
<p>CI/CD를 처음부터 구성해둔 게 팀 전체 생산성에 크게 기여했다. 수동으로 배포하느라 시간을 쓰지 않아도 됐고, 테스트가 자동으로 돌아가니 서로 코드를 합치는 데 덜 불안했다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>2주 안에 이 정도 규모의 서비스를 완성한 게 신기하게 느껴진다. 팀원 각자가 맡은 영역을 끝까지 책임졌고, 덕분에 배포 URL이 실제로 돌아간다.</p>
<p>다음에는 테스트 커버리지를 처음부터 챙기면서 만들고 싶다. </p>
<hr>
<p><a href="https://github.com/haxxru">GitHub</a> | <a href="https://wedge-tawny.vercel.app">배포 URL</a> | <a href="https://streamable.com/6dklaq">시연 영상</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 66 - Guardian]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-66-Guardian-ztimnqk3</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-66-Guardian-ztimnqk3</guid>
            <pubDate>Tue, 09 Jun 2026 09:03:37 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2025년 06월 09일 작성된 글입니다.</p>
</blockquote>
<p>Spring Security를 프로젝트에 적용하면서 CustomAuthenticationFilter, SecurityContext, JWT 인증, 인가 처리, CORS 설정까지 학습했다. 기존에 직접 구현하던 인증/인가 로직을 Spring Security가 이해할 수 있도록 연결하는 과정에 집중했다.</p>
<hr>
<h1 id="1-customauthenticationfilter-인증-처리">1. CustomAuthenticationFilter 인증 처리</h1>
<p>Spring Security가 JWT와 apiKey를 이해하지 못하기 때문에 필터를 추가하여 먼저 인증을 수행하도록 구현했다.</p>
<h2 id="핵심">핵심</h2>
<ul>
<li>CustomAuthenticationFilter는 UsernamePasswordAuthenticationFilter 이전에 실행된다.</li>
<li>accessToken 또는 apiKey가 존재하면 인증을 수행한다.</li>
<li>로그인하지 않은 요청도 통과시켜야 한다.</li>
<li>인증이 필요한지 여부는 이후 Spring Security가 판단한다.</li>
</ul>
<pre><code class="language-java">if (accessToken != null || apiKey != null) {
    // 인증 처리
}</code></pre>
<h3 id="인증-흐름">인증 흐름</h3>
<pre><code class="language-text">사용자 요청
    ↓
CustomAuthenticationFilter
    ↓
UsernamePasswordAuthenticationFilter
    ↓
인가 처리
    ↓
Controller</code></pre>
<hr>
<h1 id="2-필터에서-예외-처리">2. 필터에서 예외 처리</h1>
<p>필터는 컨트롤러보다 먼저 실행되기 때문에 ControllerAdvice가 처리할 수 없다.</p>
<h2 id="핵심-1">핵심</h2>
<ul>
<li>필터 내부에서 직접 예외 처리 필요</li>
<li>인증 실패 시 즉시 응답 반환</li>
<li>컨트롤러 진입 전에 요청 차단 가능</li>
</ul>
<pre><code class="language-java">try {
    // 인증 처리
} catch (Exception e) {
    response.setStatus(401);
}</code></pre>
<hr>
<h1 id="3-securitycontext-이해">3. SecurityContext 이해</h1>
<p>Spring Security는 현재 요청 사용자의 정보를 SecurityContext에 저장한다.</p>
<h2 id="핵심-2">핵심</h2>
<ul>
<li>인증 성공 후 사용자 정보 저장</li>
<li>인가 과정에서 권한 체크</li>
<li>요청 종료 시 자동 제거</li>
</ul>
<pre><code class="language-java">SecurityContextHolder
    .getContext()
    .setAuthentication(authentication);</code></pre>
<h3 id="동작-순서">동작 순서</h3>
<pre><code class="language-text">요청
 ↓
인증
 ↓
SecurityContext 저장
 ↓
인가
 ↓
Controller 실행
 ↓
SecurityContext 제거</code></pre>
<hr>
<h1 id="4-securityuser-도입">4. SecurityUser 도입</h1>
<p>Spring Security 기본 User 클래스에는 프로젝트에서 필요한 id, nickname 정보가 없다.</p>
<h2 id="핵심-3">핵심</h2>
<ul>
<li>UserDetails 구현</li>
<li>id 저장 가능</li>
<li>nickname 저장 가능</li>
<li>Principal에서 Member 복원 가능</li>
</ul>
<pre><code class="language-java">public class SecurityUser implements UserDetails {
    private Long id;
    private String nickname;
}</code></pre>
<hr>
<h1 id="5-선언적-인가-처리">5. 선언적 인가 처리</h1>
<p>기존에 컨트롤러에서 직접 권한을 검사하던 방식을 제거하고 Security 설정으로 이동했다.</p>
<h2 id="핵심-4">핵심</h2>
<ul>
<li>관리자 API 접근 제한</li>
<li>인증 사용자 접근 제한</li>
<li>Security 설정에서 일괄 관리</li>
</ul>
<pre><code class="language-java">.requestMatchers(&quot;/api/*/adm/**&quot;)
.hasRole(&quot;ADMIN&quot;)</code></pre>
<h3 id="url-패턴">URL 패턴</h3>
<pre><code class="language-text">*   → 한 단계
**  → 여러 단계

/api/*/adm/**</code></pre>
<p>예시</p>
<pre><code class="language-text">/api/v1/adm/members
/api/v2/adm/posts/1
/api/test/adm/stats/today</code></pre>
<hr>
<h1 id="6-인증된-사용자-복원">6. 인증된 사용자 복원</h1>
<p>CustomAuthenticationFilter가 인증을 끝냈기 때문에 Rq에서 다시 인증할 필요가 없어졌다.</p>
<h2 id="변경-전">변경 전</h2>
<pre><code class="language-java">Rq.getActor()
    -&gt; 직접 인증</code></pre>
<h2 id="변경-후">변경 후</h2>
<pre><code class="language-java">SecurityContext
    -&gt; Principal
    -&gt; Member 복원</code></pre>
<h2 id="핵심-5">핵심</h2>
<ul>
<li>인증은 필터에서 1회 수행</li>
<li>컨트롤러는 인증 결과만 사용</li>
<li>중복 제거</li>
</ul>
<hr>
<h1 id="7-rest-api-보안-설정">7. REST API 보안 설정</h1>
<p>REST API에서 사용하지 않는 Security 기능을 비활성화했다.</p>
<h2 id="핵심-6">핵심</h2>
<ul>
<li>Form Login 비활성화</li>
<li>Session 인증 비활성화</li>
<li>HTTP Basic 비활성화</li>
<li>Stateless 구조 사용</li>
</ul>
<pre><code class="language-java">http
    .formLogin(form -&gt; form.disable())
    .httpBasic(httpBasic -&gt; httpBasic.disable());</code></pre>
<hr>
<h1 id="8-쿠키-보안-강화">8. 쿠키 보안 강화</h1>
<p>apiKey 쿠키에 보안 옵션을 추가했다.</p>
<h2 id="핵심-7">핵심</h2>
<ul>
<li>HttpOnly</li>
<li>Secure</li>
<li>SameSite=Strict</li>
<li>MaxAge=1년</li>
</ul>
<pre><code class="language-java">cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setMaxAge(31536000);</code></pre>
<h3 id="samesite-옵션">SameSite 옵션</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>None</td>
<td>모든 요청 허용</td>
</tr>
<tr>
<td>Lax</td>
<td>GET 중심 허용</td>
</tr>
<tr>
<td>Strict</td>
<td>동일 사이트만 허용</td>
</tr>
</tbody></table>
<hr>
<h1 id="9-cors-설정">9. CORS 설정</h1>
<p>프론트엔드와 백엔드가 서로 다른 도메인에서 통신할 수 있도록 설정했다.</p>
<h2 id="핵심-8">핵심</h2>
<ul>
<li>허용 Origin 설정</li>
<li>허용 Header 설정</li>
<li>허용 Method 설정</li>
</ul>
<pre><code class="language-java">configuration.addAllowedOrigin(&quot;http://localhost:3000&quot;);</code></pre>
<hr>
<h1 id="10-withuserdetails-활용">10. @WithUserDetails 활용</h1>
<p>테스트 코드에서 인증 관련 반복 코드를 제거했다.</p>
<h2 id="기존">기존</h2>
<pre><code class="language-java">.header(&quot;Authorization&quot;, &quot;Bearer ...&quot;)</code></pre>
<h2 id="변경">변경</h2>
<pre><code class="language-java">@WithUserDetails(&quot;user1&quot;)</code></pre>
<h2 id="핵심-9">핵심</h2>
<ul>
<li>테스트 코드 단순화</li>
<li>인증 사용자 자동 생성</li>
<li>UserDetailsService 필요</li>
</ul>
<hr>
<h1 id="11-로그-레벨-설정">11. 로그 레벨 설정</h1>
<p>개발 환경에서는 DEBUG 로그를 사용하도록 설정했다.</p>
<h2 id="로그-레벨">로그 레벨</h2>
<table>
<thead>
<tr>
<th>레벨</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>TRACE</td>
<td>가장 상세</td>
</tr>
<tr>
<td>DEBUG</td>
<td>개발용</td>
</tr>
<tr>
<td>INFO</td>
<td>기본</td>
</tr>
<tr>
<td>WARN</td>
<td>경고</td>
</tr>
<tr>
<td>ERROR</td>
<td>오류</td>
</tr>
</tbody></table>
<pre><code class="language-yaml">logging:
  level:
    com.back: DEBUG</code></pre>
<hr>
<h1 id="12-순환참조-문제-해결">12. 순환참조 문제 해결</h1>
<p>SecurityConfig, Filter, MemberService, Rq 사이에서 순환참조가 발생했다.</p>
<h2 id="문제">문제</h2>
<pre><code class="language-text">CustomAuthenticationFilter
 ↓
Rq
 ↓
MemberService
 ↓
SecurityConfig
 ↓
CustomAuthenticationFilter</code></pre>
<h2 id="해결">해결</h2>
<ul>
<li>의존성 구조 재설계</li>
<li>순환 의존 제거</li>
<li>Security 설정 분리</li>
</ul>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>CustomAuthenticationFilter를 추가하여 JWT와 apiKey 인증을 Spring Security 앞단에서 처리했다.</li>
<li>인증 성공 시 SecurityContext에 사용자 정보를 저장하여 Spring Security가 이해할 수 있도록 연결했다.</li>
<li>SecurityUser를 도입하여 id와 nickname 같은 사용자 정보를 함께 관리할 수 있게 되었다.</li>
<li>관리자 권한과 인증 여부를 Security 설정으로 이동하여 선언적으로 인가를 처리하게 되었다.</li>
<li>REST API 환경에 맞게 Form Login, Session 인증 등을 비활성화하고 Stateless 구조를 구성했다.</li>
<li>쿠키에 Secure, HttpOnly, SameSite 옵션을 적용하여 보안을 강화했다.</li>
<li>@WithUserDetails를 사용하여 테스트 코드의 인증 관련 보일러플레이트를 제거했다.</li>
<li>Spring Security의 필터 체인 구조와 인증·인가 흐름을 실제 프로젝트에 맞게 커스터마이징하는 방법을 학습했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 65 - SecurityChain]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-65-SecurityChain-n0ml34fs</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-65-SecurityChain-n0ml34fs</guid>
            <pubDate>Mon, 08 Jun 2026 09:01:13 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 06월 08일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 기존 API Key 인증 구조에서 Spring Security 기반 인증/인가 구조로 넘어가는 과정을 정리했다.<br>관리자 API, 권한 체크, Security Filter, 비밀번호 암호화, JWT Access Token 도입 흐름까지 학습했다.</p>
<hr>
<h2 id="1-최신성이-필요한-데이터는-db-조회가-필요하다">1. 최신성이 필요한 데이터는 DB 조회가 필요하다</h2>
<p>내 정보 조회처럼 항상 최신 데이터가 필요한 경우에는 토큰에 담긴 정보만 믿지 않고 DB를 다시 조회해야 한다.</p>
<pre><code class="language-java">Member member = memberService.findById(actor.getId())
        .orElseThrow(() -&gt; new ServiceException(&quot;401-1&quot;, &quot;존재하지 않는 회원입니다.&quot;));</code></pre>
<p>토큰은 요청자를 식별하는 데 사용할 수 있지만, 닉네임이나 권한처럼 변경될 수 있는 정보는 DB에서 최신 상태를 확인하는 것이 안전하다.</p>
<hr>
<h2 id="2-관리자용-api-구현">2. 관리자용 API 구현</h2>
<p>관리자 전용 회원 조회 API와 게시물 통계 API를 구현했다.</p>
<pre><code class="language-text">GET /api/v1/adm/members
GET /api/v1/adm/members/{id}
GET /api/v1/adm/posts/count</code></pre>
<p>관리자 API는 일반 사용자 API와 구분하기 위해 <code>/adm</code> 경로를 사용했다.</p>
<hr>
<h2 id="3-관리자-권한-체크">3. 관리자 권한 체크</h2>
<p>관리자 API는 관리자 권한을 가진 사용자만 접근할 수 있도록 제한했다.</p>
<pre><code class="language-java">.andExpect(status().isForbidden())
.andExpect(jsonPath(&quot;$.resultCode&quot;).value(&quot;403-1&quot;))
.andExpect(jsonPath(&quot;$.msg&quot;).value(&quot;권한이 없습니다.&quot;));</code></pre>
<p>권한이 없는 사용자가 관리자 API를 호출하면 <code>403 Forbidden</code> 응답을 반환한다.</p>
<hr>
<h2 id="4-인증과-인가">4. 인증과 인가</h2>
<p>인증은 요청을 보낸 사용자가 누구인지 확인하는 과정이다.</p>
<pre><code class="language-text">이 요청을 보낸 너는 누구니?</code></pre>
<p>인가는 인증된 사용자가 해당 URL에 접근할 권한이 있는지 확인하는 과정이다.</p>
<pre><code class="language-text">너는 이 API에 접근할 권한이 있니?</code></pre>
<p>스프링 시큐리티에서 말하는 인가는 주로 URL 접근 권한 체크를 의미한다.</p>
<hr>
<h2 id="5-spring-security-설치">5. Spring Security 설치</h2>
<p>Spring Security 의존성을 추가했다.</p>
<pre><code class="language-gradle">implementation(&quot;org.springframework.boot:spring-boot-starter-security&quot;)
testImplementation(&quot;org.springframework.security:spring-security-test&quot;)</code></pre>
<p>Spring Security는 인증과 인가를 필터 기반으로 처리하는 보안 프레임워크다.</p>
<hr>
<h2 id="6-spring-security-기본-동작">6. Spring Security 기본 동작</h2>
<p>Spring Security를 추가하면 기본적으로 모든 요청을 보호한다.</p>
<p>즉, 별도 설정이 없으면 로그인하지 않은 사용자는 대부분의 요청에 접근할 수 없다.</p>
<p>REST API와 JWT를 사용하는 경우에는 프로젝트 방식에 맞게 직접 커스터마이징해야 한다.</p>
<hr>
<h2 id="7-security-설정">7. Security 설정</h2>
<p>REST API 프로젝트에 맞게 Security 설정을 수정했다.</p>
<pre><code class="language-java">csrf.disable();</code></pre>
<p>또한 개발 편의를 위해 h2-console 접근을 허용했다.</p>
<p>REST API에서는 보통 세션 기반 폼 로그인이 아니라 토큰 인증을 사용하므로 CSRF 설정을 비활성화하는 경우가 많다.</p>
<hr>
<h2 id="8-csrf와-samesite">8. CSRF와 SameSite</h2>
<p>CSRF는 사용자가 의도하지 않은 요청을 브라우저가 대신 보내게 만드는 공격이다.</p>
<p>REST API에서는 보통 Authorization 헤더 기반 토큰 인증을 사용하고, 쿠키를 쓰더라도 SameSite 옵션을 함께 사용한다.</p>
<table>
<thead>
<tr>
<th>SameSite</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>None</td>
<td>모든 요청에 쿠키 전송</td>
</tr>
<tr>
<td>Lax</td>
<td>일부 크로스 사이트 GET 요청 허용</td>
</tr>
<tr>
<td>Strict</td>
<td>같은 사이트 요청에만 쿠키 전송</td>
</tr>
</tbody></table>
<hr>
<h2 id="9-security-filter">9. Security Filter</h2>
<p>Spring Security는 컨트롤러가 실행되기 전에 필터에서 먼저 동작한다.</p>
<pre><code class="language-text">요청
→ Security Filter
→ Controller</code></pre>
<p>권한이 없으면 컨트롤러 액션 메서드는 실행되지 않는다.</p>
<hr>
<h2 id="10-커스텀-인증-필터가-필요한-이유">10. 커스텀 인증 필터가 필요한 이유</h2>
<p>Spring Security는 기본적으로 우리가 만든 JWT, apiKey, refreshToken 구조를 알지 못한다.</p>
<p>따라서 우리가 만든 인증 방식을 Spring Security가 이해할 수 있는 형태로 바꿔주는 커스텀 필터가 필요하다.</p>
<pre><code class="language-text">JWT / apiKey
→ CustomAuthenticationFilter
→ Spring Security 인증 객체</code></pre>
<hr>
<h2 id="11-usernamepasswordauthenticationfilter-이전에-실행">11. UsernamePasswordAuthenticationFilter 이전에 실행</h2>
<p>커스텀 필터는 Spring Security의 기본 인증 필터보다 먼저 실행되도록 등록한다.</p>
<pre><code class="language-java">addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)</code></pre>
<p>이렇게 해야 이후 Security 과정에서 인증된 사용자로 판단할 수 있다.</p>
<hr>
<h2 id="12-비밀번호-암호화">12. 비밀번호 암호화</h2>
<p>비밀번호는 복호화가 불가능한 방식으로 암호화해서 저장해야 한다.</p>
<p>로그인 시에는 사용자가 입력한 비밀번호를 같은 방식으로 암호화한 뒤 저장된 값과 비교한다.</p>
<pre><code class="language-text">입력 비밀번호 → 암호화 → 저장된 암호화 값과 비교</code></pre>
<p>비밀번호 원문을 저장하지 않기 때문에 DB가 유출되어도 피해를 줄일 수 있다.</p>
<hr>
<h2 id="13-로그-레벨">13. 로그 레벨</h2>
<p>Spring Boot의 기본 로그 레벨은 INFO다.</p>
<p>개발이나 테스트 환경에서는 DEBUG 레벨로 낮추면 더 자세한 로그를 확인할 수 있다.</p>
<table>
<thead>
<tr>
<th>레벨</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>TRACE</td>
<td>가장 상세</td>
</tr>
<tr>
<td>DEBUG</td>
<td>개발/디버깅용</td>
</tr>
<tr>
<td>INFO</td>
<td>일반 정보</td>
</tr>
<tr>
<td>WARN</td>
<td>경고</td>
</tr>
<tr>
<td>ERROR</td>
<td>에러</td>
</tr>
</tbody></table>
<p>운영 환경에서는 보통 INFO 이상만 출력한다.</p>
<hr>
<h2 id="14-인증이-필요-없는-요청은-패스">14. 인증이 필요 없는 요청은 패스</h2>
<p>로그인, 회원가입, 공개 API처럼 인증이 필요 없는 요청은 커스텀 필터에서 통과시켜야 한다.</p>
<pre><code class="language-text">인증 필요 없음 → pass
인증 필요함 → accessToken / apiKey 검사</code></pre>
<hr>
<h2 id="15-access-token과-apikey">15. Access Token과 apiKey</h2>
<p>기존 apiKey 방식은 매 요청마다 DB 조회가 필요했다.</p>
<pre><code class="language-sql">SELECT * FROM member WHERE apiKey = ?</code></pre>
<p>JWT 기반 Access Token을 사용하면 서버가 시크릿 키로 토큰을 검증할 수 있어 DB 조회 없이 인증할 수 있다.</p>
<hr>
<h2 id="16-access-token과-refresh-token">16. Access Token과 Refresh Token</h2>
<p>기존 apiKey는 Refresh Token 역할로 사용하고, JWT는 Access Token 역할로 사용할 예정이다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Access Token</th>
<th>Refresh Token</th>
</tr>
</thead>
<tbody><tr>
<td>수명</td>
<td>짧음</td>
<td>김</td>
</tr>
<tr>
<td>DB 조회</td>
<td>없음</td>
<td>필요</td>
</tr>
<tr>
<td>폐기</td>
<td>어려움</td>
<td>가능</td>
</tr>
<tr>
<td>용도</td>
<td>API 요청 인증</td>
<td>Access Token 재발급</td>
</tr>
</tbody></table>
<hr>
<h2 id="17-jwt-라이브러리-추가">17. JWT 라이브러리 추가</h2>
<p>JWT 처리를 위해 jjwt 라이브러리를 추가했다.</p>
<pre><code class="language-gradle">implementation(&quot;io.jsonwebtoken:jjwt-api:0.12.6&quot;)
runtimeOnly(&quot;io.jsonwebtoken:jjwt-impl:0.12.6&quot;)
runtimeOnly(&quot;io.jsonwebtoken:jjwt-jackson:0.12.6&quot;)</code></pre>
<hr>
<h2 id="18-jwt-생성에-필요한-정보">18. JWT 생성에 필요한 정보</h2>
<p>JWT를 만들기 위해서는 다음 정보가 필요하다.</p>
<ul>
<li>SecretKey</li>
<li>서명 알고리즘</li>
<li>Claims</li>
<li>발급 시간</li>
<li>만료 시간</li>
</ul>
<p>Claims에는 사용자 id, username 같은 인증에 필요한 데이터를 담을 수 있다.</p>
<hr>
<h2 id="19-jwt는-암호화가-아니다">19. JWT는 암호화가 아니다</h2>
<p>JWT는 암호화가 아니라 인코딩이다.</p>
<p>즉, 누구나 JWT 내용을 디코딩해서 확인할 수 있다.</p>
<pre><code class="language-text">JWT = Base64 인코딩된 토큰</code></pre>
<p>따라서 비밀번호 같은 민감 정보는 절대 JWT에 넣으면 안 된다.</p>
<hr>
<h2 id="20-순환참조-문제">20. 순환참조 문제</h2>
<p>커스텀 필터를 추가하는 과정에서 순환참조 문제가 발생했다.</p>
<pre><code class="language-text">customAuthenticationFilter
→ rq
→ memberService
→ securityConfig
→ customAuthenticationFilter</code></pre>
<p>스프링은 순환참조를 기본적으로 허용하지 않기 때문에 의존성 구조를 재설계해야 한다.</p>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>관리자 API는 일반 API와 분리하고 관리자 권한 체크를 적용해야 한다.</li>
<li>Spring Security는 필터 기반으로 컨트롤러 실행 전에 인증과 인가를 처리한다.</li>
<li>REST API + JWT 구조에서는 커스텀 필터를 통해 Spring Security가 이해할 수 있는 인증 객체를 만들어야 한다.</li>
<li>비밀번호는 복호화가 불가능한 방식으로 암호화해서 저장해야 한다.</li>
<li>Access Token은 DB 조회 없이 빠르게 인증할 수 있고, Refresh Token은 재발급과 폐기에 사용된다.</li>
<li>JWT는 암호화가 아니라 인코딩이므로 민감 정보를 담으면 안 된다.</li>
<li>순환참조가 발생하면 빈 의존성 구조를 다시 정리해야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 64 - Token]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-64-Token</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-64-Token</guid>
            <pubDate>Mon, 08 Jun 2026 08:58:55 GMT</pubDate>
            <description><![CDATA[<h1 id="tokenization">Tokenization</h1>
<blockquote>
<p>이 글은 2026년 06월 05일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 API Key 기반 인증을 쿠키 기반 인증으로 확장하고, JWT(JSON Web Token)를 이용한 Access Token 구조를 학습했다. 또한 OpenAPI와 Postman에서 인증을 처리하는 방법, 인증(Authentication)과 인가(Authorization)의 흐름을 정리했다.</p>
<hr>
<h2 id="1-api-key-쿠키-자동-전송">1. API Key 쿠키 자동 전송</h2>
<p>모든 요청에서 apiKey 쿠키가 자동으로 전송되도록 리팩토링했다.</p>
<h3 id="핵심">핵심</h3>
<ul>
<li>인증 정보 전달 코드 중복 제거</li>
<li>Rq 클래스로 쿠키 처리 로직 이동</li>
<li>모든 요청에 apiKey 쿠키 자동 포함</li>
</ul>
<hr>
<h2 id="2-cookie-기반-인증-추가">2. Cookie 기반 인증 추가</h2>
<p>기존 Authorization 헤더 인증 외에 Cookie 기반 인증도 지원하도록 개선했다.</p>
<pre><code class="language-java">.cookie(new Cookie(&quot;apiKey&quot;, apiKey))</code></pre>
<h3 id="핵심-1">핵심</h3>
<ul>
<li>Authorization 헤더 인증 지원</li>
<li>Cookie 인증 지원</li>
<li>인증 방식 다중화</li>
</ul>
<hr>
<h2 id="3-getactor-리팩토링">3. getActor 리팩토링</h2>
<p>인증 정보 조회 로직을 정리하여 가독성과 재사용성을 높였다.</p>
<h3 id="핵심-2">핵심</h3>
<ul>
<li>인증 관련 코드 분리</li>
<li>중복 제거</li>
<li>유지보수성 향상</li>
</ul>
<hr>
<h2 id="4-rq-클래스-개선">4. Rq 클래스 개선</h2>
<p>공통 요청 처리 기능을 Rq 클래스로 이동했다.</p>
<pre><code class="language-java">private String getHeader(String name, String defaultValue) {
    return Optional
            .ofNullable(req.getHeader(name))
            .filter(headerValue -&gt; !headerValue.isBlank())
            .orElse(defaultValue);
}</code></pre>
<h3 id="핵심-3">핵심</h3>
<ul>
<li>getHeader 추가</li>
<li>getCookieValue 추가</li>
<li>Optional 활용</li>
<li>null 처리 개선</li>
</ul>
<hr>
<h2 id="5-rq-클래스와-쿠키-정리">5. Rq 클래스와 쿠키 정리</h2>
<h3 id="rq-클래스">Rq 클래스</h3>
<ul>
<li>컨트롤러 공통 로직 담당</li>
<li>HttpServletRequest 래핑</li>
<li>HttpServletResponse 래핑</li>
<li>HttpSession 래핑</li>
<li>파사드(Facade) 패턴 적용</li>
</ul>
<h3 id="쿠키-특징">쿠키 특징</h3>
<ul>
<li>쿠키 관리는 브라우저가 담당</li>
<li>서버는 Set-Cookie 응답만 전달</li>
<li>브라우저가 쿠키를 저장</li>
<li>이후 요청 시 자동 전송</li>
<li>동일 도메인에서만 사용 가능</li>
</ul>
<hr>
<h2 id="6-개발-환경용-api-key-설정">6. 개발 환경용 API Key 설정</h2>
<p>개발 편의를 위해 샘플 회원의 apiKey와 username을 동일하게 설정했다.</p>
<h3 id="핵심-4">핵심</h3>
<ul>
<li>개발환경(dev) 전용 적용</li>
<li>운영환경(prod) 제외</li>
<li>테스트 편의성 향상</li>
</ul>
<hr>
<h2 id="7-static-필드-의존성-주입">7. static 필드 의존성 주입</h2>
<p>Spring은 static 필드 직접 주입을 지원하지 않는다.</p>
<h3 id="해결-방법">해결 방법</h3>
<ul>
<li>setter 메서드 생성</li>
<li>@Autowired 적용</li>
<li>setter 내부에서 static 필드 초기화</li>
</ul>
<h3 id="appconfig-활용">AppConfig 활용</h3>
<pre><code class="language-java">AppConfig.isDev()</code></pre>
<h3 id="핵심-5">핵심</h3>
<ul>
<li>Environment 직접 주입 불필요</li>
<li>가독성 향상</li>
<li>편리한 환경 체크</li>
</ul>
<hr>
<h2 id="8-로그아웃-구현">8. 로그아웃 구현</h2>
<p>HttpOnly 쿠키를 삭제하는 로그아웃 기능을 구현했다.</p>
<pre><code class="language-java">assertThat(apiKeyCookie.getMaxAge()).isEqualTo(0);</code></pre>
<h3 id="핵심-6">핵심</h3>
<ul>
<li>DELETE 요청 사용</li>
<li>Set-Cookie 응답 전송</li>
<li>Max-Age=0 설정</li>
<li>브라우저가 자동 삭제</li>
</ul>
<hr>
<h2 id="9-openapi에서-로그인로그아웃">9. OpenAPI에서 로그인/로그아웃</h2>
<p>Swagger(OpenAPI) 환경에서도 로그인과 로그아웃이 가능하도록 설정했다.</p>
<h3 id="핵심-7">핵심</h3>
<ul>
<li>로그인 시 apiKey 쿠키 생성</li>
<li>API 테스트 가능</li>
<li>로그아웃 시 쿠키 삭제</li>
</ul>
<hr>
<h2 id="10-securityscheme-적용">10. @SecurityScheme 적용</h2>
<p>OpenAPI에서 Authorization 헤더를 직접 입력할 수 있도록 설정했다.</p>
<pre><code class="language-java">@SecurityScheme</code></pre>
<h3 id="핵심-8">핵심</h3>
<ul>
<li>Swagger 인증 지원</li>
<li>Bearer Token 입력 가능</li>
<li>API 테스트 편의성 향상</li>
</ul>
<hr>
<h2 id="11-인증-흐름-정리">11. 인증 흐름 정리</h2>
<h3 id="인증-정보-우선순위">인증 정보 우선순위</h3>
<ol>
<li>Authorization 헤더</li>
<li>Cookie(apiKey)</li>
</ol>
<h3 id="핵심-9">핵심</h3>
<ul>
<li>헤더 인증 우선</li>
<li>쿠키 인증 보조</li>
<li>둘 다 없으면 인증 실패</li>
</ul>
<hr>
<h2 id="12-jwt-기반-access-token-도입">12. JWT 기반 Access Token 도입</h2>
<p>기존 apiKey 방식의 단점을 해결하기 위해 JWT 기반 Access Token을 도입한다.</p>
<h3 id="기존-방식">기존 방식</h3>
<pre><code class="language-sql">SELECT * FROM member WHERE apiKey = ?</code></pre>
<h3 id="문제점">문제점</h3>
<ul>
<li>매 요청마다 DB 조회</li>
<li>서버 부하 증가</li>
</ul>
<hr>
<h2 id="13-access-token과-refresh-token">13. Access Token과 Refresh Token</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>Access Token</th>
<th>Refresh Token</th>
</tr>
</thead>
<tbody><tr>
<td>수명</td>
<td>짧음</td>
<td>김</td>
</tr>
<tr>
<td>DB 조회</td>
<td>없음</td>
<td>있음</td>
</tr>
<tr>
<td>폐기</td>
<td>어려움</td>
<td>가능</td>
</tr>
<tr>
<td>목적</td>
<td>API 인증</td>
<td>토큰 재발급</td>
</tr>
</tbody></table>
<h3 id="핵심-10">핵심</h3>
<ul>
<li>JWT = Access Token</li>
<li>apiKey = Refresh Token</li>
<li>인증 성능 향상</li>
</ul>
<hr>
<h2 id="14-jwt-특징">14. JWT 특징</h2>
<p>JWT(JSON Web Token)는 인증 정보를 담은 토큰이다.</p>
<h3 id="jwt-구성-요소">JWT 구성 요소</h3>
<ul>
<li>Header</li>
<li>Payload(Claims)</li>
<li>Signature</li>
</ul>
<h3 id="핵심-11">핵심</h3>
<ul>
<li>서버 저장 불필요</li>
<li>Stateless 인증</li>
<li>DB 조회 없음</li>
<li>빠른 인증 처리</li>
</ul>
<hr>
<h2 id="15-jwt-라이브러리-추가">15. JWT 라이브러리 추가</h2>
<pre><code class="language-gradle">implementation(&quot;io.jsonwebtoken:jjwt-api:0.12.6&quot;)
runtimeOnly(&quot;io.jsonwebtoken:jjwt-impl:0.12.6&quot;)
runtimeOnly(&quot;io.jsonwebtoken:jjwt-jackson:0.12.6&quot;)</code></pre>
<h3 id="핵심-12">핵심</h3>
<ul>
<li>JWT 생성</li>
<li>JWT 검증</li>
<li>JWT 파싱</li>
</ul>
<hr>
<h2 id="16-jwt-생성-테스트">16. JWT 생성 테스트</h2>
<p>JWT를 생성하고 디코딩하여 내부 데이터를 확인했다.</p>
<h3 id="jwt에-포함되는-정보">JWT에 포함되는 정보</h3>
<ul>
<li>사용자 정보</li>
<li>발급 시간(iat)</li>
<li>만료 시간(exp)</li>
<li>기타 Claims</li>
</ul>
<h3 id="주의사항">주의사항</h3>
<p>JWT는 암호화가 아니다.</p>
<p>JWT는 Base64 인코딩이므로 누구나 내용을 확인할 수 있다.</p>
<p>따라서 비밀번호와 같은 민감 정보는 절대 저장하면 안 된다.</p>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>API Key 인증을 Cookie 기반 인증으로 확장하였다.</li>
<li>Authorization 헤더와 Cookie 인증을 모두 지원하도록 개선하였다.</li>
<li>Rq 클래스로 요청 처리 공통 로직을 분리하였다.</li>
<li>OpenAPI에서도 로그인과 로그아웃을 수행할 수 있도록 설정하였다.</li>
<li>JWT 기반 Access Token 구조를 도입하기 위한 기반 작업을 진행하였다.</li>
<li>JWT는 서버 저장 없이 검증 가능한 Stateless 인증 방식이다.</li>
<li>기존 apiKey는 Refresh Token 역할로 활용할 예정이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 63 - Tokenization]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-63-Tokenization-ea9uutgq</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-63-Tokenization-ea9uutgq</guid>
            <pubDate>Thu, 04 Jun 2026 07:33:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 06월 01일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 API Key 기반 인증을 쿠키 기반 인증으로 확장하고, JWT(JSON Web Token)를 이용한 Access Token 구조를 학습했다. 또한 OpenAPI와 Postman에서 인증을 처리하는 방법, 인증(Authentication)과 인가(Authorization)의 흐름을 정리했다.</p>
<hr>
<h2 id="1-api-key-쿠키-자동-전송">1. API Key 쿠키 자동 전송</h2>
<p>모든 요청에서 apiKey 쿠키가 자동으로 전송되도록 리팩토링했다.</p>
<h3 id="핵심">핵심</h3>
<ul>
<li>인증 정보 전달 코드 중복 제거</li>
<li>Rq 클래스로 쿠키 처리 로직 이동</li>
<li>모든 요청에 apiKey 쿠키 자동 포함</li>
</ul>
<hr>
<h2 id="2-cookie-기반-인증-추가">2. Cookie 기반 인증 추가</h2>
<p>기존 Authorization 헤더 인증 외에 Cookie 기반 인증도 지원하도록 개선했다.</p>
<pre><code class="language-java">.cookie(new Cookie(&quot;apiKey&quot;, apiKey))</code></pre>
<h3 id="핵심-1">핵심</h3>
<ul>
<li>Authorization 헤더 인증 지원</li>
<li>Cookie 인증 지원</li>
<li>인증 방식 다중화</li>
</ul>
<hr>
<h2 id="3-getactor-리팩토링">3. getActor 리팩토링</h2>
<p>인증 정보 조회 로직을 정리하여 가독성과 재사용성을 높였다.</p>
<h3 id="핵심-2">핵심</h3>
<ul>
<li>인증 관련 코드 분리</li>
<li>중복 제거</li>
<li>유지보수성 향상</li>
</ul>
<hr>
<h2 id="4-rq-클래스-개선">4. Rq 클래스 개선</h2>
<p>공통 요청 처리 기능을 Rq 클래스로 이동했다.</p>
<pre><code class="language-java">private String getHeader(String name, String defaultValue) {
    return Optional
            .ofNullable(req.getHeader(name))
            .filter(headerValue -&gt; !headerValue.isBlank())
            .orElse(defaultValue);
}</code></pre>
<h3 id="핵심-3">핵심</h3>
<ul>
<li>getHeader 추가</li>
<li>getCookieValue 추가</li>
<li>Optional 활용</li>
<li>null 처리 개선</li>
</ul>
<hr>
<h2 id="5-rq-클래스와-쿠키-정리">5. Rq 클래스와 쿠키 정리</h2>
<h3 id="rq-클래스">Rq 클래스</h3>
<ul>
<li>컨트롤러 공통 로직 담당</li>
<li>HttpServletRequest 래핑</li>
<li>HttpServletResponse 래핑</li>
<li>HttpSession 래핑</li>
<li>파사드(Facade) 패턴 적용</li>
</ul>
<h3 id="쿠키-특징">쿠키 특징</h3>
<ul>
<li>쿠키 관리는 브라우저가 담당</li>
<li>서버는 Set-Cookie 응답만 전달</li>
<li>브라우저가 쿠키를 저장</li>
<li>이후 요청 시 자동 전송</li>
<li>동일 도메인에서만 사용 가능</li>
</ul>
<hr>
<h2 id="6-개발-환경용-api-key-설정">6. 개발 환경용 API Key 설정</h2>
<p>개발 편의를 위해 샘플 회원의 apiKey와 username을 동일하게 설정했다.</p>
<h3 id="핵심-4">핵심</h3>
<ul>
<li>개발환경(dev) 전용 적용</li>
<li>운영환경(prod) 제외</li>
<li>테스트 편의성 향상</li>
</ul>
<hr>
<h2 id="7-static-필드-의존성-주입">7. static 필드 의존성 주입</h2>
<p>Spring은 static 필드 직접 주입을 지원하지 않는다.</p>
<h3 id="해결-방법">해결 방법</h3>
<ul>
<li>setter 메서드 생성</li>
<li>@Autowired 적용</li>
<li>setter 내부에서 static 필드 초기화</li>
</ul>
<h3 id="appconfig-활용">AppConfig 활용</h3>
<pre><code class="language-java">AppConfig.isDev()</code></pre>
<h3 id="핵심-5">핵심</h3>
<ul>
<li>Environment 직접 주입 불필요</li>
<li>가독성 향상</li>
<li>편리한 환경 체크</li>
</ul>
<hr>
<h2 id="8-로그아웃-구현">8. 로그아웃 구현</h2>
<p>HttpOnly 쿠키를 삭제하는 로그아웃 기능을 구현했다.</p>
<pre><code class="language-java">assertThat(apiKeyCookie.getMaxAge()).isEqualTo(0);</code></pre>
<h3 id="핵심-6">핵심</h3>
<ul>
<li>DELETE 요청 사용</li>
<li>Set-Cookie 응답 전송</li>
<li>Max-Age=0 설정</li>
<li>브라우저가 자동 삭제</li>
</ul>
<hr>
<h2 id="9-openapi에서-로그인로그아웃">9. OpenAPI에서 로그인/로그아웃</h2>
<p>Swagger(OpenAPI) 환경에서도 로그인과 로그아웃이 가능하도록 설정했다.</p>
<h3 id="핵심-7">핵심</h3>
<ul>
<li>로그인 시 apiKey 쿠키 생성</li>
<li>API 테스트 가능</li>
<li>로그아웃 시 쿠키 삭제</li>
</ul>
<hr>
<h2 id="10-securityscheme-적용">10. @SecurityScheme 적용</h2>
<p>OpenAPI에서 Authorization 헤더를 직접 입력할 수 있도록 설정했다.</p>
<pre><code class="language-java">@SecurityScheme</code></pre>
<h3 id="핵심-8">핵심</h3>
<ul>
<li>Swagger 인증 지원</li>
<li>Bearer Token 입력 가능</li>
<li>API 테스트 편의성 향상</li>
</ul>
<hr>
<h2 id="11-인증-흐름-정리">11. 인증 흐름 정리</h2>
<h3 id="인증-정보-우선순위">인증 정보 우선순위</h3>
<ol>
<li>Authorization 헤더</li>
<li>Cookie(apiKey)</li>
</ol>
<h3 id="핵심-9">핵심</h3>
<ul>
<li>헤더 인증 우선</li>
<li>쿠키 인증 보조</li>
<li>둘 다 없으면 인증 실패</li>
</ul>
<hr>
<h2 id="12-jwt-기반-access-token-도입">12. JWT 기반 Access Token 도입</h2>
<p>기존 apiKey 방식의 단점을 해결하기 위해 JWT 기반 Access Token을 도입한다.</p>
<h3 id="기존-방식">기존 방식</h3>
<pre><code class="language-sql">SELECT * FROM member WHERE apiKey = ?</code></pre>
<h3 id="문제점">문제점</h3>
<ul>
<li>매 요청마다 DB 조회</li>
<li>서버 부하 증가</li>
</ul>
<hr>
<h2 id="13-access-token과-refresh-token">13. Access Token과 Refresh Token</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>Access Token</th>
<th>Refresh Token</th>
</tr>
</thead>
<tbody><tr>
<td>수명</td>
<td>짧음</td>
<td>김</td>
</tr>
<tr>
<td>DB 조회</td>
<td>없음</td>
<td>있음</td>
</tr>
<tr>
<td>폐기</td>
<td>어려움</td>
<td>가능</td>
</tr>
<tr>
<td>목적</td>
<td>API 인증</td>
<td>토큰 재발급</td>
</tr>
</tbody></table>
<h3 id="핵심-10">핵심</h3>
<ul>
<li>JWT = Access Token</li>
<li>apiKey = Refresh Token</li>
<li>인증 성능 향상</li>
</ul>
<hr>
<h2 id="14-jwt-특징">14. JWT 특징</h2>
<p>JWT(JSON Web Token)는 인증 정보를 담은 토큰이다.</p>
<h3 id="jwt-구성-요소">JWT 구성 요소</h3>
<ul>
<li>Header</li>
<li>Payload(Claims)</li>
<li>Signature</li>
</ul>
<h3 id="핵심-11">핵심</h3>
<ul>
<li>서버 저장 불필요</li>
<li>Stateless 인증</li>
<li>DB 조회 없음</li>
<li>빠른 인증 처리</li>
</ul>
<hr>
<h2 id="15-jwt-라이브러리-추가">15. JWT 라이브러리 추가</h2>
<pre><code class="language-gradle">implementation(&quot;io.jsonwebtoken:jjwt-api:0.12.6&quot;)
runtimeOnly(&quot;io.jsonwebtoken:jjwt-impl:0.12.6&quot;)
runtimeOnly(&quot;io.jsonwebtoken:jjwt-jackson:0.12.6&quot;)</code></pre>
<h3 id="핵심-12">핵심</h3>
<ul>
<li>JWT 생성</li>
<li>JWT 검증</li>
<li>JWT 파싱</li>
</ul>
<hr>
<h2 id="16-jwt-생성-테스트">16. JWT 생성 테스트</h2>
<p>JWT를 생성하고 디코딩하여 내부 데이터를 확인했다.</p>
<h3 id="jwt에-포함되는-정보">JWT에 포함되는 정보</h3>
<ul>
<li>사용자 정보</li>
<li>발급 시간(iat)</li>
<li>만료 시간(exp)</li>
<li>기타 Claims</li>
</ul>
<h3 id="주의사항">주의사항</h3>
<p>JWT는 암호화가 아니다.</p>
<p>JWT는 Base64 인코딩이므로 누구나 내용을 확인할 수 있다.</p>
<p>따라서 비밀번호와 같은 민감 정보는 절대 저장하면 안 된다.</p>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>API Key 인증을 Cookie 기반 인증으로 확장하였다.</li>
<li>Authorization 헤더와 Cookie 인증을 모두 지원하도록 개선하였다.</li>
<li>Rq 클래스로 요청 처리 공통 로직을 분리하였다.</li>
<li>OpenAPI에서도 로그인과 로그아웃을 수행할 수 있도록 설정하였다.</li>
<li>JWT 기반 Access Token 구조를 도입하기 위한 기반 작업을 진행하였다.</li>
<li>JWT는 서버 저장 없이 검증 가능한 Stateless 인증 방식이다.</li>
<li>기존 apiKey는 Refresh Token 역할로 활용할 예정이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 62 - AuthFlow pt.2]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-62-AuthFlow-pt.2-yp1q2xyb</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-62-AuthFlow-pt.2-yp1q2xyb</guid>
            <pubDate>Tue, 02 Jun 2026 10:57:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 06월 02일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 REST API 인증(Authentication)과 인가(Authorization)를 구현하며 API Key 기반 인증 구조를 적용했다. 또한 회원가입, 로그인 API를 구현하고 Postman을 활용한 인증 테스트 방법을 학습했다.</p>
<hr>
<h2 id="1-dto-변경-시-테스트도-함께-수정">1. DTO 변경 시 테스트도 함께 수정</h2>
<p>DTO 구조가 변경되면 API 응답 형식도 변경되므로 테스트 코드 역시 함께 수정해야 한다.</p>
<h3 id="핵심">핵심</h3>
<ul>
<li>DTO 변경 시 테스트 코드도 수정</li>
<li>응답 JSON 검증 유지</li>
<li>테스트와 실제 API 스펙 동기화</li>
</ul>
<hr>
<h2 id="2-usernamepassword-인증-방식">2. username/password 인증 방식</h2>
<p>초기에는 글 작성 시 username과 password를 요청 파라미터로 전달하여 사용자를 식별했다.</p>
<pre><code class="language-json">{
  &quot;username&quot;: &quot;user1&quot;,
  &quot;password&quot;: &quot;1234&quot;
}</code></pre>
<h3 id="문제점">문제점</h3>
<ul>
<li>매 요청마다 인증정보 전송</li>
<li>클라이언트에 비밀번호 저장 필요</li>
<li>보안상 위험</li>
<li>요청 데이터가 불필요하게 커짐</li>
</ul>
<hr>
<h2 id="3-serviceexception과-전역-예외-처리">3. ServiceException과 전역 예외 처리</h2>
<p>오류 응답 형식을 통일하기 위해 ServiceException을 도입하고 GlobalExceptionHandler에서 처리했다.</p>
<h3 id="핵심-1">핵심</h3>
<ul>
<li>예외 응답 형식 통일</li>
<li>중복 코드 제거</li>
<li>유지보수성 향상</li>
</ul>
<hr>
<h2 id="4-api-key-인증-방식-도입">4. API Key 인증 방식 도입</h2>
<p>username/password 대신 apiKey를 사용하도록 구조를 변경했다.</p>
<pre><code class="language-java">@Column(unique = true)
private String apiKey;</code></pre>
<h3 id="핵심-2">핵심</h3>
<ul>
<li>회원마다 고유 API Key 발급</li>
<li>비밀번호 대신 API Key 사용</li>
<li>클라이언트에는 API Key만 저장</li>
</ul>
<hr>
<h2 id="5-api-key-방식의-장점">5. API Key 방식의 장점</h2>
<h3 id="핵심-3">핵심</h3>
<ul>
<li>비밀번호 노출 위험 감소</li>
<li>사용자 비밀번호 저장 불필요</li>
<li>인증정보 관리 단순화</li>
<li>피해 범위 축소 가능</li>
</ul>
<hr>
<h2 id="6-회원가입-api-구현">6. 회원가입 API 구현</h2>
<p>회원가입 기능을 테스트 코드와 함께 구현했다.</p>
<pre><code class="language-java">POST /api/v1/members</code></pre>
<h3 id="핵심-4">핵심</h3>
<ul>
<li>회원 생성</li>
<li>username 중복 검사</li>
<li>서비스 계층으로 검증 로직 이동</li>
</ul>
<hr>
<h2 id="7-로그인-api-구현">7. 로그인 API 구현</h2>
<p>로그인 성공 시 API Key를 반환하도록 구현했다.</p>
<pre><code class="language-java">POST /api/v1/members/login</code></pre>
<p>응답 예시</p>
<pre><code class="language-json">{
  &quot;resultCode&quot;: &quot;200-1&quot;,
  &quot;msg&quot;: &quot;유저1님 환영합니다.&quot;,
  &quot;data&quot;: {
    &quot;apiKey&quot;: &quot;...&quot;
  }
}</code></pre>
<h3 id="핵심-5">핵심</h3>
<ul>
<li>로그인 성공 시 API Key 반환</li>
<li>이후 요청에서 API Key 사용</li>
</ul>
<hr>
<h2 id="8-postman-api-key-자동-저장">8. Postman API Key 자동 저장</h2>
<p>로그인 성공 후 API Key를 자동으로 컬렉션 변수에 저장하도록 설정했다.</p>
<pre><code class="language-javascript">const apiKey = pm.response.json()?.data?.apiKey;

if (apiKey) {
    pm.collectionVariables.set(&quot;apiKey&quot;, apiKey);
}</code></pre>
<h3 id="핵심-6">핵심</h3>
<ul>
<li>로그인 후 자동 갱신</li>
<li>수동 복사 제거</li>
<li>테스트 편의성 향상</li>
</ul>
<hr>
<h2 id="9-authorization-헤더-사용">9. Authorization 헤더 사용</h2>
<p>인증정보를 요청 파라미터가 아닌 HTTP 헤더에 저장하도록 변경했다.</p>
<pre><code class="language-http">Authorization: Bearer {apiKey}</code></pre>
<h3 id="핵심-7">핵심</h3>
<ul>
<li>REST API 표준 방식</li>
<li>인증정보와 요청 데이터 분리</li>
<li>보안성 향상</li>
</ul>
<hr>
<h2 id="10-requestheader-활용">10. @RequestHeader 활용</h2>
<pre><code class="language-java">@RequestHeader(&quot;Authorization&quot;)
String authorization</code></pre>
<h3 id="핵심-8">핵심</h3>
<ul>
<li>HTTP 헤더 값 주입</li>
<li>Authorization 헤더 처리</li>
<li>Bearer Token 인증 구현</li>
</ul>
<hr>
<h2 id="11-인증정보-상속-구조postman">11. 인증정보 상속 구조(Postman)</h2>
<p>Postman에서는 인증정보를 상위 레벨에서 설정할 수 있다.</p>
<h3 id="핵심-9">핵심</h3>
<ul>
<li>Collection → Folder → Request 순서 상속</li>
<li>중복 설정 제거</li>
<li>유지보수 편리</li>
</ul>
<hr>
<h2 id="12-인증authentication과-인가authorization">12. 인증(Authentication)과 인가(Authorization)</h2>
<h3 id="인증">인증</h3>
<p>사용자가 누구인지 확인하는 과정</p>
<p>예시</p>
<ul>
<li>로그인</li>
<li>API Key 검증</li>
<li>토큰 검증</li>
</ul>
<h3 id="인가">인가</h3>
<p>인증된 사용자의 권한을 확인하는 과정</p>
<p>예시</p>
<ul>
<li>본인 글만 수정 가능</li>
<li>본인 댓글만 삭제 가능</li>
</ul>
<hr>
<h2 id="13-글-수정-권한-체크">13. 글 수정 권한 체크</h2>
<p>글 작성자만 수정 가능하도록 인가 로직을 추가했다.</p>
<h3 id="핵심-10">핵심</h3>
<ul>
<li>API Key 인증</li>
<li>작성자 검증</li>
<li>권한 없는 접근 차단</li>
</ul>
<hr>
<h2 id="14-글-삭제-권한-체크">14. 글 삭제 권한 체크</h2>
<p>글 작성자만 삭제 가능하도록 수정했다.</p>
<h3 id="핵심-11">핵심</h3>
<ul>
<li>작성자 검증</li>
<li>권한 체크</li>
<li>테스트 코드 수정</li>
</ul>
<hr>
<h2 id="15-댓글-api-인증-적용">15. 댓글 API 인증 적용</h2>
<p>댓글 등록 역시 API Key 인증 기반으로 변경했다.</p>
<h3 id="핵심-12">핵심</h3>
<ul>
<li>댓글 작성 인증</li>
<li>API Key 기반 사용자 식별</li>
<li>테스트 코드 수정</li>
</ul>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>username/password 방식 대신 API Key 인증 구조를 도입했다.</li>
<li>ServiceException과 GlobalExceptionHandler로 예외 응답을 통일했다.</li>
<li>회원가입과 로그인 API를 구현하고 API Key를 발급하도록 만들었다.</li>
<li>Authorization 헤더를 활용하여 인증정보를 표준 방식으로 전달하도록 변경했다.</li>
<li>Postman 컬렉션 변수를 활용해 API Key를 자동 관리할 수 있게 만들었다.</li>
<li>글과 댓글 수정·삭제 시 작성자 권한 검사를 통해 인가 기능을 구현했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 61 - AuthFlow]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-61-AuthFlow</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-61-AuthFlow</guid>
            <pubDate>Mon, 01 Jun 2026 07:18:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 06월 01일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 REST API 인증(Authentication)과 인가(Authorization)를 구현하며 API Key 기반 인증 구조를 적용했다. 또한 회원가입, 로그인 API를 구현하고 Postman을 활용한 인증 테스트 방법을 학습했다.</p>
<hr>
<h2 id="1-dto-변경-시-테스트도-함께-수정">1. DTO 변경 시 테스트도 함께 수정</h2>
<p>DTO 구조가 변경되면 API 응답 형식도 변경되므로 테스트 코드 역시 함께 수정해야 한다.</p>
<h3 id="핵심">핵심</h3>
<ul>
<li>DTO 변경 시 테스트 코드도 수정</li>
<li>응답 JSON 검증 유지</li>
<li>테스트와 실제 API 스펙 동기화</li>
</ul>
<hr>
<h2 id="2-usernamepassword-인증-방식">2. username/password 인증 방식</h2>
<p>초기에는 글 작성 시 username과 password를 요청 파라미터로 전달하여 사용자를 식별했다.</p>
<pre><code class="language-json">{
  &quot;username&quot;: &quot;user1&quot;,
  &quot;password&quot;: &quot;1234&quot;
}</code></pre>
<h3 id="문제점">문제점</h3>
<ul>
<li>매 요청마다 인증정보 전송</li>
<li>클라이언트에 비밀번호 저장 필요</li>
<li>보안상 위험</li>
<li>요청 데이터가 불필요하게 커짐</li>
</ul>
<hr>
<h2 id="3-serviceexception과-전역-예외-처리">3. ServiceException과 전역 예외 처리</h2>
<p>오류 응답 형식을 통일하기 위해 ServiceException을 도입하고 GlobalExceptionHandler에서 처리했다.</p>
<h3 id="핵심-1">핵심</h3>
<ul>
<li>예외 응답 형식 통일</li>
<li>중복 코드 제거</li>
<li>유지보수성 향상</li>
</ul>
<hr>
<h2 id="4-api-key-인증-방식-도입">4. API Key 인증 방식 도입</h2>
<p>username/password 대신 apiKey를 사용하도록 구조를 변경했다.</p>
<pre><code class="language-java">@Column(unique = true)
private String apiKey;</code></pre>
<h3 id="핵심-2">핵심</h3>
<ul>
<li>회원마다 고유 API Key 발급</li>
<li>비밀번호 대신 API Key 사용</li>
<li>클라이언트에는 API Key만 저장</li>
</ul>
<hr>
<h2 id="5-api-key-방식의-장점">5. API Key 방식의 장점</h2>
<h3 id="핵심-3">핵심</h3>
<ul>
<li>비밀번호 노출 위험 감소</li>
<li>사용자 비밀번호 저장 불필요</li>
<li>인증정보 관리 단순화</li>
<li>피해 범위 축소 가능</li>
</ul>
<hr>
<h2 id="6-회원가입-api-구현">6. 회원가입 API 구현</h2>
<p>회원가입 기능을 테스트 코드와 함께 구현했다.</p>
<pre><code class="language-java">POST /api/v1/members</code></pre>
<h3 id="핵심-4">핵심</h3>
<ul>
<li>회원 생성</li>
<li>username 중복 검사</li>
<li>서비스 계층으로 검증 로직 이동</li>
</ul>
<hr>
<h2 id="7-로그인-api-구현">7. 로그인 API 구현</h2>
<p>로그인 성공 시 API Key를 반환하도록 구현했다.</p>
<pre><code class="language-java">POST /api/v1/members/login</code></pre>
<p>응답 예시</p>
<pre><code class="language-json">{
  &quot;resultCode&quot;: &quot;200-1&quot;,
  &quot;msg&quot;: &quot;유저1님 환영합니다.&quot;,
  &quot;data&quot;: {
    &quot;apiKey&quot;: &quot;...&quot;
  }
}</code></pre>
<h3 id="핵심-5">핵심</h3>
<ul>
<li>로그인 성공 시 API Key 반환</li>
<li>이후 요청에서 API Key 사용</li>
</ul>
<hr>
<h2 id="8-postman-api-key-자동-저장">8. Postman API Key 자동 저장</h2>
<p>로그인 성공 후 API Key를 자동으로 컬렉션 변수에 저장하도록 설정했다.</p>
<pre><code class="language-javascript">const apiKey = pm.response.json()?.data?.apiKey;

if (apiKey) {
    pm.collectionVariables.set(&quot;apiKey&quot;, apiKey);
}</code></pre>
<h3 id="핵심-6">핵심</h3>
<ul>
<li>로그인 후 자동 갱신</li>
<li>수동 복사 제거</li>
<li>테스트 편의성 향상</li>
</ul>
<hr>
<h2 id="9-authorization-헤더-사용">9. Authorization 헤더 사용</h2>
<p>인증정보를 요청 파라미터가 아닌 HTTP 헤더에 저장하도록 변경했다.</p>
<pre><code class="language-http">Authorization: Bearer {apiKey}</code></pre>
<h3 id="핵심-7">핵심</h3>
<ul>
<li>REST API 표준 방식</li>
<li>인증정보와 요청 데이터 분리</li>
<li>보안성 향상</li>
</ul>
<hr>
<h2 id="10-requestheader-활용">10. @RequestHeader 활용</h2>
<pre><code class="language-java">@RequestHeader(&quot;Authorization&quot;)
String authorization</code></pre>
<h3 id="핵심-8">핵심</h3>
<ul>
<li>HTTP 헤더 값 주입</li>
<li>Authorization 헤더 처리</li>
<li>Bearer Token 인증 구현</li>
</ul>
<hr>
<h2 id="11-인증정보-상속-구조postman">11. 인증정보 상속 구조(Postman)</h2>
<p>Postman에서는 인증정보를 상위 레벨에서 설정할 수 있다.</p>
<h3 id="핵심-9">핵심</h3>
<ul>
<li>Collection → Folder → Request 순서 상속</li>
<li>중복 설정 제거</li>
<li>유지보수 편리</li>
</ul>
<hr>
<h2 id="12-인증authentication과-인가authorization">12. 인증(Authentication)과 인가(Authorization)</h2>
<h3 id="인증">인증</h3>
<p>사용자가 누구인지 확인하는 과정</p>
<p>예시</p>
<ul>
<li>로그인</li>
<li>API Key 검증</li>
<li>토큰 검증</li>
</ul>
<h3 id="인가">인가</h3>
<p>인증된 사용자의 권한을 확인하는 과정</p>
<p>예시</p>
<ul>
<li>본인 글만 수정 가능</li>
<li>본인 댓글만 삭제 가능</li>
</ul>
<hr>
<h2 id="13-글-수정-권한-체크">13. 글 수정 권한 체크</h2>
<p>글 작성자만 수정 가능하도록 인가 로직을 추가했다.</p>
<h3 id="핵심-10">핵심</h3>
<ul>
<li>API Key 인증</li>
<li>작성자 검증</li>
<li>권한 없는 접근 차단</li>
</ul>
<hr>
<h2 id="14-글-삭제-권한-체크">14. 글 삭제 권한 체크</h2>
<p>글 작성자만 삭제 가능하도록 수정했다.</p>
<h3 id="핵심-11">핵심</h3>
<ul>
<li>작성자 검증</li>
<li>권한 체크</li>
<li>테스트 코드 수정</li>
</ul>
<hr>
<h2 id="15-댓글-api-인증-적용">15. 댓글 API 인증 적용</h2>
<p>댓글 등록 역시 API Key 인증 기반으로 변경했다.</p>
<h3 id="핵심-12">핵심</h3>
<ul>
<li>댓글 작성 인증</li>
<li>API Key 기반 사용자 식별</li>
<li>테스트 코드 수정</li>
</ul>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>username/password 방식 대신 API Key 인증 구조를 도입했다.</li>
<li>ServiceException과 GlobalExceptionHandler로 예외 응답을 통일했다.</li>
<li>회원가입과 로그인 API를 구현하고 API Key를 발급하도록 만들었다.</li>
<li>Authorization 헤더를 활용하여 인증정보를 표준 방식으로 전달하도록 변경했다.</li>
<li>Postman 컬렉션 변수를 활용해 API Key를 자동 관리할 수 있게 만들었다.</li>
<li>글과 댓글 수정·삭제 시 작성자 권한 검사를 통해 인가 기능을 구현했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 60 - Connect]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-60-Connect-11mtcjy8</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-60-Connect-11mtcjy8</guid>
            <pubDate>Mon, 01 Jun 2026 02:33:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 05월 29일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 Next.js 프론트엔드와 Spring Boot REST API를 연동하여 게시글과 댓글의 CRUD 기능을 구현했다. 또한 DTO 설계 원칙과 API 응답 구조에 대해 학습했다.</p>
<hr>
<h2 id="1-apifetch-확장">1. apiFetch 확장</h2>
<p>기존 GET 요청만 처리하던 apiFetch 함수를 확장하여 POST 요청도 처리할 수 있도록 개선했다.</p>
<pre><code class="language-ts">await apiFetch(&quot;/api/v1/posts&quot;, {
    method: &quot;POST&quot;,
    body: JSON.stringify(data),
});</code></pre>
<h3 id="핵심">핵심</h3>
<ul>
<li>GET, POST 요청을 공통 함수로 처리</li>
<li>기본 headers 설정 추가</li>
<li>중복 코드 감소</li>
</ul>
<hr>
<h2 id="2-게시글-작성-기능">2. 게시글 작성 기능</h2>
<p>게시글 작성 후 성공 메시지를 출력하고 상세 페이지로 이동하도록 구현했다.</p>
<pre><code class="language-ts">router.replace(`/posts/${id}`);</code></pre>
<h3 id="핵심-1">핵심</h3>
<ul>
<li>작성 완료 후 상세 페이지 이동</li>
<li>replace 사용으로 뒤로가기 시 작성 페이지 재진입 방지</li>
<li>autofocus 적용</li>
</ul>
<hr>
<h2 id="3-게시글-삭제-기능">3. 게시글 삭제 기능</h2>
<p>게시글 삭제 API와 연동하고 사용자 확인 절차를 추가했다.</p>
<pre><code class="language-ts">if (confirm(&quot;정말 삭제하시겠습니까?&quot;)) {
    // 삭제 요청
}</code></pre>
<h3 id="핵심-2">핵심</h3>
<ul>
<li>삭제 전 확인 창 제공</li>
<li>삭제 완료 후 목록 페이지 이동</li>
</ul>
<hr>
<h2 id="4-게시글-수정-기능">4. 게시글 수정 기능</h2>
<p>수정 페이지를 구현하고 기존 데이터를 불러와 수정 API와 연동했다.</p>
<pre><code class="language-ts">setTitle(post.title);
setContent(post.content);</code></pre>
<h3 id="핵심-3">핵심</h3>
<ul>
<li>수정 폼 구현</li>
<li>기존 데이터 자동 입력</li>
<li>수정 API 호출</li>
</ul>
<hr>
<h2 id="5-댓글-목록-조회">5. 댓글 목록 조회</h2>
<p>댓글 다건 조회 API를 연동하고 로딩 상태를 처리했다.</p>
<pre><code class="language-ts">const [comments, setComments] = useState&lt;PostCommentDto[] | null&gt;(null);</code></pre>
<h3 id="핵심-4">핵심</h3>
<ul>
<li>댓글 목록 API 연동</li>
<li>로딩 상태 표시</li>
<li>댓글 DTO 타입 정의</li>
</ul>
<hr>
<h2 id="6-상태-초기값을-null로-두는-이유">6. 상태 초기값을 null로 두는 이유</h2>
<pre><code class="language-ts">const [comments, setComments] = useState&lt;PostCommentDto[] | null&gt;(null);</code></pre>
<h3 id="핵심-5">핵심</h3>
<ul>
<li>null → 로딩 중</li>
<li>[] → 데이터 없음</li>
</ul>
<p>초기값을 빈 배열로 두면 로딩 중인지 데이터가 없는지 구분할 수 없다.</p>
<hr>
<h2 id="7-댓글-삭제">7. 댓글 삭제</h2>
<p>댓글 삭제 후 상태값을 직접 수정하여 화면에 즉시 반영되도록 구현했다.</p>
<pre><code class="language-ts">setComments(
    comments.filter(comment =&gt; comment.id !== id)
);</code></pre>
<h3 id="핵심-6">핵심</h3>
<ul>
<li>새로고침 없이 UI 갱신</li>
<li>상태 기반 렌더링 활용</li>
</ul>
<hr>
<h2 id="8-댓글-등록">8. 댓글 등록</h2>
<p>댓글 작성 폼을 구현하고 등록 즉시 화면에 반영되도록 처리했다.</p>
<pre><code class="language-ts">setComments([...comments, newComment]);</code></pre>
<h3 id="핵심-7">핵심</h3>
<ul>
<li>댓글 등록 API 연동</li>
<li>실시간 UI 갱신</li>
</ul>
<hr>
<h2 id="9-dto-설계-원칙">9. DTO 설계 원칙</h2>
<p>DTO에는 꼭 필요한 데이터만 포함해야 한다.</p>
<p>좋은 예시</p>
<pre><code class="language-json">{
    &quot;id&quot;: 4,
    &quot;authorName&quot;: &quot;유저3&quot;,
    &quot;content&quot;: &quot;댓글 2-1&quot;
}</code></pre>
<p>좋지 않은 예시</p>
<pre><code class="language-json">{
    &quot;author&quot;: {
        ...
    },
    &quot;post&quot;: {
        ...
    }
}</code></pre>
<h3 id="핵심-8">핵심</h3>
<ul>
<li>최소 공개 원칙</li>
<li>필요한 데이터만 응답</li>
<li>엔티티 직접 노출 지양</li>
</ul>
<hr>
<h2 id="10-작성자-정보-추가">10. 작성자 정보 추가</h2>
<p>게시글과 댓글에 작성자 정보를 추가했다.</p>
<pre><code class="language-java">@ManyToOne
private Member author;</code></pre>
<h3 id="핵심-9">핵심</h3>
<ul>
<li>게시글 작성자 관리</li>
<li>댓글 작성자 관리</li>
<li>DTO에는 authorName만 노출</li>
</ul>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>apiFetch를 확장하여 GET과 POST 요청을 공통 처리할 수 있게 만들었다.</li>
<li>게시글 작성, 수정, 삭제 기능을 REST API와 연동했다.</li>
<li>댓글 조회, 등록, 삭제 기능을 구현하고 상태 변경으로 UI를 즉시 갱신했다.</li>
<li>상태 초기값을 null로 두어 로딩 상태와 빈 데이터를 구분할 수 있게 했다.</li>
<li>DTO에는 필요한 데이터만 담아 최소 공개 원칙을 적용했다.</li>
<li>게시글과 댓글에 작성자 정보를 추가하여 데이터 구조를 확장했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 59 - Spring-Next]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-59-Spring-Next</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-59-Spring-Next</guid>
            <pubDate>Thu, 28 May 2026 09:05:00 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 05월 28일 작성된 글입니다.</p>
</blockquote>
<h2 id="📌-오늘-배운-내용">📌 오늘 배운 내용</h2>
<p>오늘은 Next.js 기반 프론트엔드 프로젝트를 세팅하고, Spring Boot REST API 서버와 연동하여 게시글 목록/상세/작성 기능을 구현했다.<br>또한 서버 컴포넌트와 클라이언트 컴포넌트의 차이, fetch 기반 데이터 요청, 환경변수 관리, 동적 라우팅 등을 학습했다.</p>
<hr>
<h2 id="1-nextjs-프로젝트-생성-및-실행">1. Next.js 프로젝트 생성 및 실행</h2>
<h3 id="프로젝트-생성">프로젝트 생성</h3>
<pre><code class="language-bash">npx create-next-app@latest</code></pre>
<h3 id="실행">실행</h3>
<pre><code class="language-bash">npm run dev</code></pre>
<hr>
<h2 id="2-prettier-설정">2. Prettier 설정</h2>
<pre><code class="language-json">{
    &quot;tabWidth&quot;: 4,
    &quot;trailingComma&quot;: &quot;all&quot;,
    &quot;semi&quot;: false,
    &quot;singleQuote&quot;: true
}</code></pre>
<hr>
<h2 id="3-서버-컴포넌트와-클라이언트-컴포넌트">3. 서버 컴포넌트와 클라이언트 컴포넌트</h2>
<pre><code class="language-tsx">&#39;use client&#39;</code></pre>
<table>
<thead>
<tr>
<th>구분</th>
<th>서버 컴포넌트</th>
<th>클라이언트 컴포넌트</th>
</tr>
</thead>
<tbody><tr>
<td>상태관리</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>이벤트</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>렌더링</td>
<td>서버</td>
<td>브라우저</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-글-목록-api-연동">4. 글 목록 API 연동</h2>
<pre><code class="language-tsx">useEffect(() =&gt; {
    fetch(&#39;http://localhost:8080/api/v1/posts&#39;)
        .then((res) =&gt; res.json())
        .then((data) =&gt; setPosts(data))
}, [])</code></pre>
<hr>
<h2 id="5-동적-라우팅">5. 동적 라우팅</h2>
<pre><code class="language-text">src/app/posts/[id]/page.tsx</code></pre>
<hr>
<h2 id="6-guard-clause-패턴">6. Guard Clause 패턴</h2>
<pre><code class="language-tsx">if (post == null) {
    return &lt;div&gt;로딩중...&lt;/div&gt;
}

return &lt;div&gt;{post.title}&lt;/div&gt;</code></pre>
<hr>
<h2 id="7-환경변수-사용">7. 환경변수 사용</h2>
<pre><code class="language-env">NEXT_PUBLIC_API_BASE_URL=http://localhost:8080</code></pre>
<hr>
<h2 id="8-실제-글-작성-api-요청">8. 실제 글 작성 API 요청</h2>
<pre><code class="language-tsx">fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/posts`, {
    method: &#39;POST&#39;,
    headers: {
        &#39;Content-Type&#39;: &#39;application/json&#39;,
    },
    body: JSON.stringify({
        title: titleInput.value,
        content: contentInput.value,
    }),
})</code></pre>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>Next.js App Router 기반 프로젝트 구조를 학습했다.</li>
<li>서버 컴포넌트와 클라이언트 컴포넌트의 차이를 이해했다.</li>
<li>REST API 데이터를 fetch로 받아 렌더링했다.</li>
<li>동적 라우팅과 환경변수를 적용했다.</li>
<li>POST 요청으로 실제 글 작성 기능을 구현했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 58 - Contract pt.3]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-58-Contract-pt.3-gof7wpcd</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-58-Contract-pt.3-gof7wpcd</guid>
            <pubDate>Wed, 27 May 2026 08:15:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 05월 27일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 REST API 테스트 강화와 Swagger 문서 자동화,
그리고 IoC 컨테이너 구조 확장을 진행했다.</p>
<hr>
<h2 id="1-댓글-삭제-기능-구현">1. 댓글 삭제 기능 구현</h2>
<p>댓글 삭제 API를 구현했다.</p>
<pre><code class="language-java">@Transactional
public void delete(PostComment postComment)</code></pre>
<hr>
<h2 id="2-orphanremoval-과-더티체킹">2. orphanRemoval 과 더티체킹</h2>
<pre><code class="language-java">@OneToMany(orphanRemoval = true)</code></pre>
<p>@Transactional 이 있어야 더티체킹이 동작한다.</p>
<hr>
<h2 id="3-조회-전용-트랜잭션">3. 조회 전용 트랜잭션</h2>
<pre><code class="language-java">@Transactional(readOnly = true)</code></pre>
<p>조회 메서드 성능 최적화 적용.</p>
<hr>
<h2 id="4-rsdata-응답-구조">4. RsData 응답 구조</h2>
<pre><code class="language-json">{
  &quot;resultCode&quot;: &quot;200-1&quot;,
  &quot;msg&quot;: &quot;댓글 삭제 완료&quot;
}</code></pre>
<p>공통 응답 구조 적용.</p>
<hr>
<h2 id="5-제네릭-적용">5. 제네릭 적용</h2>
<pre><code class="language-java">RsData&lt;PostDto&gt;</code></pre>
<p>타입 안정성 강화.</p>
<hr>
<h2 id="6-delete-메서드-적용">6. DELETE 메서드 적용</h2>
<pre><code class="language-java">@DeleteMapping</code></pre>
<p>REST 스타일로 개선.</p>
<hr>
<h2 id="7-postman-활용">7. POSTMAN 활용</h2>
<pre><code class="language-text">/api/v1/posts</code></pre>
<p>API 테스트 진행.</p>
<hr>
<h2 id="8-json-요청-처리">8. JSON 요청 처리</h2>
<pre><code class="language-java">@RequestBody</code></pre>
<p>JSON → 객체 자동 변환.</p>
<hr>
<h2 id="9-validation-처리">9. Validation 처리</h2>
<pre><code class="language-java">@Valid</code></pre>
<p>REST API에서는 BindingResult를 거의 사용하지 않는다.</p>
<hr>
<h2 id="10-springdoc-도입">10. SpringDoc 도입</h2>
<pre><code class="language-kotlin">implementation(&quot;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9&quot;)</code></pre>
<p>Swagger 기반 OPEN API 문서 자동 생성.</p>
<hr>
<h2 id="11-swagger-ui">11. Swagger UI</h2>
<pre><code class="language-text">http://localhost:8080/swagger-ui/index.html</code></pre>
<p>브라우저에서 API 테스트 가능.</p>
<hr>
<h2 id="12-cors-처리">12. CORS 처리</h2>
<pre><code class="language-text">Access-Control-Allow-Origin</code></pre>
<p>외부 fetch 요청 허용 설정.</p>
<hr>
<h1 id="ioc-컨테이너-확장">IoC 컨테이너 확장</h1>
<h2 id="13-beandefinition-기반-구조">13. BeanDefinition 기반 구조</h2>
<pre><code class="language-java">Map&lt;String, BeanDefinition&gt;</code></pre>
<p>빈 생성 정보를 저장.</p>
<hr>
<h2 id="14-bean-메서드-기반-생성">14. @Bean 메서드 기반 생성</h2>
<pre><code class="language-java">new BeanDefinition&lt;&gt;(
    TestJacksonConfig.class,
    &quot;testBaseJavaTimeModule&quot;
)</code></pre>
<p>메서드 기반 빈 생성 지원.</p>
<hr>
<h2 id="15-reflections-라이브러리-활용">15. reflections 라이브러리 활용</h2>
<pre><code class="language-kotlin">implementation(&quot;org.reflections:reflections:0.10.2&quot;)</code></pre>
<p>패키지 스캔 자동화 구현.</p>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>REST API에서는 상태코드와 응답 구조를 명확하게 관리하는 것이 중요하다.</li>
<li>SpringDoc을 통해 Swagger 기반 문서를 자동 생성할 수 있었다.</li>
<li>CORS 설정을 통해 다른 서버에서 오는 fetch 요청도 허용할 수 있었다.</li>
<li>IoC 컨테이너는 BeanDefinition 기반 구조로 점점 실제 스프링과 비슷하게 발전하고 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 57 - Contract pt.2]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-57-Contract-uu0zfooa</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-57-Contract-uu0zfooa</guid>
            <pubDate>Tue, 26 May 2026 08:12:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 05월 26일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 REST API 테스트를 더 정교하게 다듬고,
예외 처리와 HTTP 상태코드 처리,
그리고 IoC 컨테이너의 BeanDefinition 구조를 확장하는 작업을 진행했다.</p>
<hr>
<h2 id="1-글-수정-테스트케이스-보강">1. 글 수정 테스트케이스 보강</h2>
<p>글 수정 API 테스트를 더 강화했다.</p>
<p>기존에는 응답만 확인했다면,
이제는 실제로 수정된 데이터가 DB에 반영되었는지도 검증했다.</p>
<pre><code class="language-java">assertThat(post.getTitle()).isEqualTo(&quot;수정된 제목&quot;);</code></pre>
<hr>
<h2 id="2-테스트-검증-범위">2. 테스트 검증 범위</h2>
<p>REST API 테스트에서 어디까지 검증해야 하는지 기준을 정리했다.</p>
<h3 id="필수">필수</h3>
<p>응답 JSON 구조와 필드값 검증</p>
<pre><code class="language-json">{
  &quot;resultCode&quot;: &quot;200-1&quot;,
  &quot;msg&quot;: &quot;글 수정 완료&quot;
}</code></pre>
<h3 id="선택">선택</h3>
<p>실제 DB 조회 후 데이터 검증</p>
<pre><code class="language-java">Post post = postService.findById(1);</code></pre>
<p>응답 구조 검증은 필수,
DB 반영 여부까지 확인하는 것은 상황에 따라 선택이다.</p>
<hr>
<h2 id="3-글-삭제-테스트">3. 글 삭제 테스트</h2>
<p>글 삭제 API 테스트를 추가했다.</p>
<pre><code class="language-java">mockMvc.perform(delete(&quot;/api/v1/posts/1&quot;))</code></pre>
<hr>
<h2 id="4-단건-조회-테스트">4. 단건 조회 테스트</h2>
<p>단건 조회 테스트도 추가했다.</p>
<pre><code class="language-java">.andExpect(jsonPath(&quot;$.data.id&quot;).value(1))</code></pre>
<hr>
<h2 id="5-다건-조회-테스트">5. 다건 조회 테스트</h2>
<p>게시글 목록 조회 테스트를 구현했다.</p>
<pre><code class="language-java">.andExpect(jsonPath(&quot;$.data.length()&quot;).value(3))</code></pre>
<hr>
<h2 id="6-postman-baseurl-변수">6. Postman baseURL 변수</h2>
<p>Postman Collection에 <code>baseURL</code> 변수를 추가했다.</p>
<pre><code class="language-text">{{baseURL}}/api/v1/posts</code></pre>
<hr>
<h2 id="7-존재하지-않는-글-조회">7. 존재하지 않는 글 조회</h2>
<p>존재하지 않는 글 조회 시 <code>404</code> 응답을 반환하도록 수정했다.</p>
<pre><code class="language-java">return ResponseEntity.status(404).body(rsData);</code></pre>
<hr>
<h2 id="8-validation-예외-처리">8. Validation 예외 처리</h2>
<p>제목 없이 글 작성 시 <code>400 Bad Request</code>가 발생하도록 구현했다.</p>
<pre><code class="language-java">@NotBlank
private String title;</code></pre>
<hr>
<h2 id="9-methodargumentnotvalidexception-처리">9. MethodArgumentNotValidException 처리</h2>
<p>유효성 검증 실패 시 발생하는 예외를 처리했다.</p>
<pre><code class="language-java">@ExceptionHandler(MethodArgumentNotValidException.class)</code></pre>
<hr>
<h2 id="10-fielderror-메세지-처리">10. FieldError 메세지 처리</h2>
<p>검증 에러를 문자열 형태로 정리했다.</p>
<pre><code class="language-java">.map(error -&gt; error.getField()
        + &quot;-&quot; + error.getCode()
        + &quot;-&quot; + error.getDefaultMessage())</code></pre>
<hr>
<h2 id="11-잘못된-json-처리">11. 잘못된 JSON 처리</h2>
<p>JSON 형식이 잘못된 경우에도 <code>400</code> 응답을 반환하도록 처리했다.</p>
<hr>
<h2 id="12-댓글-단건조회-테스트">12. 댓글 단건조회 테스트</h2>
<p>댓글 API 테스트도 시작했다.</p>
<pre><code class="language-text">/api/v1/posts/1/comments/2</code></pre>
<hr>
<h2 id="13-댓글-다건조회-테스트">13. 댓글 다건조회 테스트</h2>
<p>댓글 목록 조회 테스트 추가.</p>
<pre><code class="language-text">/api/v1/posts/1/comments</code></pre>
<hr>
<h2 id="14-beandefinition-메서드-기반-생성">14. BeanDefinition 메서드 기반 생성</h2>
<p><code>@Bean</code> 메서드 기반 생성도 처리할 수 있도록 확장했다.</p>
<pre><code class="language-java">new BeanDefinition&lt;&gt;(
    TestJacksonConfig.class,
    &quot;testBaseJavaTimeModule&quot;
);</code></pre>
<hr>
<h2 id="15-생성-의존성이-있는-beandefinition">15. 생성 의존성이 있는 BeanDefinition</h2>
<pre><code class="language-java">assertThat(beanDefinition.getParameterNames())
    .containsExactly(&quot;testBaseJavaTimeModule&quot;);</code></pre>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>REST API 테스트에서는 응답 JSON 구조 검증이 가장 중요하다.</li>
<li>Validation 예외는 MethodArgumentNotValidException으로 처리할 수 있다.</li>
<li>ResponseEntity를 사용하면 상태코드와 응답 구조를 더 유연하게 관리할 수 있다.</li>
<li>BeanDefinition이 확장되면서 @Bean 메서드 기반 빈 생성도 처리할 수 있게 되었다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 56 - Contract]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-56-Contract</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-56-Contract</guid>
            <pubDate>Fri, 22 May 2026 07:30:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 05월 22일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 IoC 컨테이너의 <code>@Bean</code> 처리 구조와
REST API 응답 구조 개선, AOP, DTO 활용 기준,
그리고 컨트롤러 TDD 흐름까지 정리했다.</p>
<hr>
<h2 id="1-jackson-라이브러리-추가">1. Jackson 라이브러리 추가</h2>
<p>Java 객체를 JSON으로 변환하기 위해 Jackson 라이브러리를 추가했다.</p>
<pre><code class="language-kotlin">implementation(&quot;com.fasterxml.jackson.core:jackson-databind:2.18.2&quot;)
implementation(&quot;com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2&quot;)</code></pre>
<p><code>jackson-datatype-jsr310</code>은 <code>LocalDateTime</code> 같은 Java Time API 타입을 처리하기 위해 필요하다.</p>
<hr>
<h2 id="2-bean-어노테이션-추가">2. @Bean 어노테이션 추가</h2>
<p>기존에는 <code>@Component</code> 계열 클래스만 빈으로 등록했다.</p>
<p>이제는 설정 클래스 안의 메서드에 <code>@Bean</code>을 붙여서
메서드의 반환 객체도 빈으로 등록할 수 있도록 구조를 확장했다.</p>
<pre><code class="language-java">@Configuration
public class TestJacksonConfig {
    @Bean
    public JavaTimeModule testBaseJavaTimeModule() {
        return new JavaTimeModule();
    }
}</code></pre>
<hr>
<h2 id="3-objectmapper-빈-등록">3. ObjectMapper 빈 등록</h2>
<p><code>ObjectMapper</code>를 직접 생성하고,
<code>JavaTimeModule</code>을 등록한 뒤 빈으로 관리하도록 했다.</p>
<pre><code class="language-java">@Bean
public ObjectMapper testBaseObjectMapper(JavaTimeModule testBaseJavaTimeModule) {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(testBaseJavaTimeModule);
    return objectMapper;
}</code></pre>
<p>이제 <code>ObjectMapper</code>도 IoC 컨테이너가 관리하는 객체가 되었다.</p>
<hr>
<h2 id="4-bean-의존관계-테스트">4. @Bean 의존관계 테스트</h2>
<p><code>testBaseObjectMapper</code>는 <code>testBaseJavaTimeModule</code> 빈에 의존한다.</p>
<pre><code class="language-java">@Test
@DisplayName(&quot;@Bean, testBaseJavaTimeModule 빈에 의존하는 testBaseObjectMapper 빈을 생성&quot;)
public void t8() {
    ObjectMapper testBaseObjectMapper = applicationContext.genBean(&quot;testBaseObjectMapper&quot;);

    assertThat(testBaseObjectMapper).isNotNull();
}</code></pre>
<p><code>@Bean</code> 메서드도 생성자 주입처럼 필요한 의존 빈을 매개변수로 받을 수 있다.</p>
<hr>
<h2 id="5-beandefinition-도입">5. BeanDefinition 도입</h2>
<p>빈 생성 방식이 늘어나면서 단순히 <code>Class</code> 정보만으로는 빈을 만들기 어려워졌다.</p>
<p>기존 방식:</p>
<ul>
<li><code>@Component</code> 계열 클래스의 생성자 기반 빈 생성</li>
</ul>
<p>새 방식:</p>
<ul>
<li><code>@Bean</code> 메서드 기반 빈 생성</li>
</ul>
<p>그래서 빈 생성에 필요한 정보를 담기 위한 <code>BeanDefinition</code> 클래스를 도입했다.</p>
<pre><code class="language-java">BeanDefinition&lt;TestPostService&gt; beanDefinition =
    new BeanDefinition&lt;&gt;(TestPostService.class);</code></pre>
<hr>
<h2 id="6-beandefinitiongetparameternames">6. BeanDefinition.getParameterNames()</h2>
<p><code>BeanDefinition</code>에서 빈 생성에 필요한 의존성 이름을 가져올 수 있게 했다.</p>
<pre><code class="language-java">String[] parameterNames = beanDefinition.getParameterNames();</code></pre>
<p>테스트 예시:</p>
<pre><code class="language-java">assertThat(parameterNames).containsExactly(&quot;testPostRepository&quot;);</code></pre>
<p>이를 통해 해당 빈을 만들기 위해 어떤 빈이 먼저 필요한지 알 수 있다.</p>
<hr>
<h2 id="7-beandefinition의-생성-방식-구분">7. BeanDefinition의 생성 방식 구분</h2>
<p><code>BeanDefinition</code>에 다음 기능을 추가했다.</p>
<pre><code class="language-java">beanDefinition.getBeanName();
beanDefinition.isCreateTypeMethod();</code></pre>
<p>예시:</p>
<pre><code class="language-java">assertThat(beanDefinition.getBeanName()).isEqualTo(&quot;testPostService&quot;);
assertThat(beanDefinition.isCreateTypeMethod()).isFalse();</code></pre>
<p><code>isCreateTypeMethod()</code>는 빈 생성 방식이 메서드 기반인지 확인하기 위한 기능이다.</p>
<hr>
<h2 id="8-rsdata의-한계">8. RsData의 한계</h2>
<p>기존 <code>RsData</code>의 <code>data</code> 필드에는 하나의 값만 담을 수 있었다.</p>
<p>하지만 프론트에서 글 작성 후 다음 데이터를 함께 원할 수 있다.</p>
<ul>
<li>생성된 게시글</li>
<li>전체 게시글 수</li>
<li>추가 메타 정보</li>
</ul>
<p>이런 경우 단순히 <code>data</code> 하나만으로는 표현이 애매해질 수 있다.</p>
<hr>
<h2 id="9-resbody-클래스-도입">9. ResBody 클래스 도입</h2>
<p>복잡한 응답 데이터를 담기 위해 액션 메서드 전용 응답 본문 클래스를 두는 방식으로 개선했다.</p>
<p>처음에는 <code>Map</code>을 사용할 수도 있지만,
명확한 응답 구조를 위해 전용 클래스를 만드는 것이 더 좋다.</p>
<pre><code class="language-java">public record PostWriteResBody(
    PostDto post,
    long totalCount
) {
}</code></pre>
<ul>
<li>응답 구조 명확화</li>
<li>타입 안정성 증가</li>
<li>프론트와의 API 계약 관리 쉬움</li>
</ul>
<hr>
<h2 id="10-http-상태코드">10. HTTP 상태코드</h2>
<p>게시글 작성 성공 시에는 <code>201 Created</code>가 가장 의미상 적절하다.</p>
<table>
<thead>
<tr>
<th>상태코드</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>200</td>
<td>OK</td>
</tr>
<tr>
<td>201</td>
<td>Created</td>
</tr>
<tr>
<td>204</td>
<td>No Content</td>
</tr>
<tr>
<td>400</td>
<td>Bad Request</td>
</tr>
<tr>
<td>401</td>
<td>Unauthorized</td>
</tr>
<tr>
<td>403</td>
<td>Forbidden</td>
</tr>
<tr>
<td>404</td>
<td>Not Found</td>
</tr>
<tr>
<td>422</td>
<td>Unprocessable Entity</td>
</tr>
<tr>
<td>500</td>
<td>Internal Server Error</td>
</tr>
</tbody></table>
<p>실제로는 <code>200</code>으로 응답해도 동작에는 문제가 없지만,
의미를 정확히 표현하려면 <code>201</code>이 더 적절하다.</p>
<hr>
<h2 id="11-responseentity">11. ResponseEntity</h2>
<p>HTTP 상태코드를 직접 지정하기 위해 <code>ResponseEntity</code>를 사용할 수 있다.</p>
<pre><code class="language-java">return ResponseEntity.status(201).body(rsData);</code></pre>
<p>기존 응답 본문 구조를 유지하면서 HTTP 상태코드를 원하는 값으로 바꿀 수 있다.</p>
<hr>
<h2 id="12-responseaspect-도입">12. ResponseAspect 도입</h2>
<p><code>RsData</code>의 <code>resultCode</code>를 기준으로 HTTP 상태코드를 자동 반영하기 위해 AOP를 도입했다.</p>
<pre><code class="language-java">@Aspect
@Component
public class ResponseAspect {
}</code></pre>
<p>컨트롤러 메서드가 <code>RsData</code>를 반환하면
그 안의 statusCode를 <code>HttpServletResponse</code>에 반영한다.</p>
<pre><code class="language-java">if (proceed instanceof RsData&lt;?&gt; rsData) {
    response.setStatus(rsData.statusCode());
}</code></pre>
<hr>
<h2 id="13-aop-직접-사용">13. AOP 직접 사용</h2>
<p>스프링에서 AOP는 간접적으로 자주 사용한다.</p>
<p>예시:</p>
<ul>
<li><code>@Transactional</code></li>
<li><code>@Cacheable</code></li>
</ul>
<p>이번에는 <code>@Aspect</code>를 직접 만들어서 응답 후처리 로직을 구현했다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>간접 사용</th>
<th>직접 사용</th>
</tr>
</thead>
<tbody><tr>
<td>예시</td>
<td><code>@Transactional</code></td>
<td><code>@Aspect</code></td>
</tr>
<tr>
<td>난이도</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>유연성</td>
<td>제한적</td>
<td>높음</td>
</tr>
</tbody></table>
<hr>
<h2 id="14-statuscode는-json에서-제외">14. statusCode는 JSON에서 제외</h2>
<p><code>RsData</code> 내부의 <code>statusCode</code>는 HTTP 응답 상태코드 설정에만 사용된다.</p>
<p>응답 JSON에는 굳이 노출할 필요가 없으므로 <code>@JsonIgnore</code>를 적용했다.</p>
<pre><code class="language-java">@JsonIgnore
public int statusCode() {
    return statusCode;
}</code></pre>
<hr>
<h2 id="15-dto-활용-기준">15. DTO 활용 기준</h2>
<p>REST API에서 DTO는 역할에 따라 나눌 수 있다.</p>
<h3 id="rsdata">RsData</h3>
<p>조회 API를 제외한 대부분의 요청 응답에 사용한다.</p>
<pre><code class="language-java">RsData&lt;Void&gt;
RsData&lt;PostDto&gt;
RsData&lt;List&lt;PostDto&gt;&gt;
RsData&lt;PostWriteResBody&gt;</code></pre>
<h3 id="엔티티-dto">엔티티 DTO</h3>
<p>엔티티를 외부에 직접 노출하지 않기 위해 사용한다.</p>
<pre><code class="language-java">PostDto
MemberDto</code></pre>
<h3 id="요청-본문-dto">요청 본문 DTO</h3>
<p>JSON 요청을 받을 때 사용한다.</p>
<pre><code class="language-java">PostWriteReqBody
PostModifyReqBody</code></pre>
<h3 id="응답-본문-dto">응답 본문 DTO</h3>
<p>응답 데이터가 복잡할 때만 만든다.</p>
<pre><code class="language-java">PostWriteResBody
PostModifyResBody</code></pre>
<hr>
<h2 id="16-댓글-수정-api-구현">16. 댓글 수정 API 구현</h2>
<p>댓글 수정 API를 구현했다.</p>
<p>댓글 작성, 삭제에 이어 수정까지 구현하면서 댓글 CRUD 흐름이 더 완성되었다.</p>
<hr>
<h2 id="17-컨트롤러-tdd-시작">17. 컨트롤러 TDD 시작</h2>
<p>REST API 컨트롤러 테스트를 시작했다.</p>
<pre><code class="language-java">@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles(&quot;test&quot;)
@Transactional
class ApiV1PostControllerTest {
}</code></pre>
<p>주요 테스트 환경:</p>
<ul>
<li><code>@SpringBootTest</code></li>
<li><code>@AutoConfigureMockMvc</code></li>
<li><code>@ActiveProfiles(&quot;test&quot;)</code></li>
<li><code>@Transactional</code></li>
</ul>
<hr>
<h2 id="18-mockmvc">18. MockMvc</h2>
<p><code>MockMvc</code>를 사용하면 실제 서버를 띄우지 않고도 HTTP 요청과 응답을 테스트할 수 있다.</p>
<pre><code class="language-java">mockMvc.perform(post(&quot;/api/v1/posts&quot;))</code></pre>
<p>컨트롤러 테스트는 크게 두 단계로 볼 수 있다.</p>
<ol>
<li>요청을 날린다.</li>
<li>결과를 검증한다.</li>
</ol>
<hr>
<h2 id="19-글-작성-테스트">19. 글 작성 테스트</h2>
<p>POST 요청으로 글 작성 API를 테스트했다.</p>
<pre><code class="language-java">resultActions
    .andExpect(status().isCreated());</code></pre>
<p>또한 <code>andDo(print())</code>를 추가해서 요청과 응답 내용을 확인했다.</p>
<hr>
<h2 id="20-응답-json-전체-검증">20. 응답 JSON 전체 검증</h2>
<p>REST API 테스트에서는 응답 JSON의 모든 필드를 검사하는 것이 좋다.</p>
<p>이유:</p>
<ul>
<li>프론트와의 API 계약 보호</li>
<li>필드 누락 방지</li>
<li>응답 구조 변경 감지</li>
<li>회귀 버그 조기 발견</li>
</ul>
<p>테스트 코드는 API 문서 역할도 함께 한다.</p>
<hr>
<h2 id="21-자동화-테스트의-장점">21. 자동화 테스트의 장점</h2>
<p><code>ResponseAspect</code>처럼 프로젝트 전반에 영향을 주는 코드를 수정할 때
자동화된 테스트가 안전망 역할을 한다.</p>
<p>테스트가 있으면 리팩토링 시 기존 기능이 깨졌는지 빠르게 확인할 수 있다.</p>
<hr>
<h2 id="22-요구사항-변경과-tdd">22. 요구사항 변경과 TDD</h2>
<p>기존 기능의 요구사항이 바뀌면 구현보다 테스트를 먼저 수정하는 흐름이 좋다.</p>
<p>예시:</p>
<ul>
<li>더 이상 <code>totalCount</code>가 필요 없음</li>
<li><code>PostWriteResBody</code> 제거</li>
<li>테스트 실패 확인</li>
<li>구현 수정</li>
<li>테스트 통과</li>
</ul>
<p>이 흐름이 TDD의 안정적인 변경 방식이다.</p>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li><code>@Bean</code> 방식이 추가되면서 빈 생성 정보를 표현하기 위해 <code>BeanDefinition</code>이 필요해졌다.</li>
<li>Jackson과 ObjectMapper를 빈으로 등록하면서 설정 객체도 IoC 컨테이너가 관리할 수 있게 되었다.</li>
<li><code>RsData</code>와 응답 DTO를 분리하면 REST API 응답 구조를 더 명확하게 관리할 수 있다.</li>
<li>AOP를 활용하면 <code>RsData</code>의 resultCode를 기반으로 HTTP 상태코드를 자동 반영할 수 있다.</li>
<li>REST API 테스트에서는 응답 JSON의 모든 필드를 검증하여 프론트와의 API 계약을 지킬 수 있다.</li>
<li>자동화 테스트가 있으면 전역적인 리팩토링과 요구사항 변경에도 더 안전하게 대응할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 55 - Response]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-55-Response-srfsth3t</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-55-Response-srfsth3t</guid>
            <pubDate>Wed, 20 May 2026 07:50:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 05월 20일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 ApplicationContext의 동적 빈 탐색 개선과
REST API 삭제/작성 요청 처리, RsData 응답 구조까지 정리했다.</p>
<hr>
<h2 id="1-applicationcontext-하드코딩-제거">1. ApplicationContext 하드코딩 제거</h2>
<p>기존에는 ApplicationContext 내부에서
특정 클래스 이름이나 생성 방식을 하드코딩하는 부분이 있었다.</p>
<p>이를 <code>ClsUtil</code>을 활용하는 방식으로 변경하여
코드를 더 유연하게 만들었다.</p>
<pre><code class="language-java">ClsUtil.construct(cls, args);</code></pre>
<ul>
<li>하드코딩 감소</li>
<li>동적 객체 생성 가능</li>
<li>IoC 컨테이너 구조 개선</li>
</ul>
<hr>
<h2 id="2-테스트용-패키지-이동">2. 테스트용 패키지 이동</h2>
<p><code>com.ll.domain.testPost.testPost</code> 패키지를
테스트 폴더로 옮길 수 있게 되어 구조를 정리했다.</p>
<p>테스트 전용 클래스는 실제 애플리케이션 코드와 분리하는 것이 좋다.</p>
<ul>
<li>테스트 코드와 운영 코드 분리</li>
<li>프로젝트 구조 명확화</li>
</ul>
<hr>
<h2 id="3-reflections-라이브러리-추가">3. Reflections 라이브러리 추가</h2>
<p>패키지 스캔을 위해 <code>org.reflections:reflections</code> 라이브러리를 추가했다.</p>
<pre><code class="language-gradle">implementation(&quot;org.reflections:reflections:0.10.2&quot;)
implementation(&quot;ch.qos.logback:logback-classic:1.5.12&quot;)</code></pre>
<p>이 라이브러리를 사용하면
특정 패키지 하위에서 어노테이션이 붙은 클래스를 자동으로 찾을 수 있다.</p>
<hr>
<h2 id="4-component-계열-클래스-스캔">4. @Component 계열 클래스 스캔</h2>
<p><code>@Component</code>가 붙은 클래스를 스캔하여
빈 후보로 수집했다.</p>
<pre><code class="language-java">Reflections reflections = new Reflections(&quot;com.ll&quot;, TypesAnnotated);

Map&lt;String, Class&lt;?&gt;&gt; clsMap = reflections
    .getTypesAnnotatedWith(Component.class)
    .stream()
    .filter(cls -&gt; !cls.isAnnotation())
    .collect(
        LinkedHashMap::new,
        (map, cls) -&gt; map.put(Ut.str.lcfirst(cls.getSimpleName()), cls),
        Map::putAll
    );</code></pre>
<p><code>@Service</code>, <code>@Repository</code>처럼
<code>@Component</code>를 기반으로 한 어노테이션도 함께 탐색할 수 있다.</p>
<hr>
<h2 id="5-빈-이름-규칙">5. 빈 이름 규칙</h2>
<p>스프링 빈 이름 규칙처럼
클래스명의 첫 글자를 소문자로 바꾸어 빈 이름을 만들었다.</p>
<pre><code class="language-java">MemberService -&gt; memberService</code></pre>
<p>이를 위해 <code>lcfirst</code> 유틸을 구현했다.</p>
<pre><code class="language-java">public static String lcfirst(String str) {
    if (str == null || str.isEmpty()) return str;

    char firstChar = str.charAt(0);

    if (Character.isLowerCase(firstChar)) return str;

    return Character.toLowerCase(firstChar) + str.substring(1);
}</code></pre>
<hr>
<h2 id="6-annotatedclasses-함수-구현">6. annotatedClasses 함수 구현</h2>
<p>특정 패키지 하위에서
특정 어노테이션을 가진 클래스를 수집하는 함수를 구현했다.</p>
<pre><code class="language-java">Map&lt;String, Class&lt;?&gt;&gt; annotatedClasses =
    ClsUtil.annotatedClasses(&quot;com.ll&quot;, Component.class);</code></pre>
<p>테스트에서는 다음 빈들이 잘 수집되는지 확인했다.</p>
<ul>
<li>testFacadePostService</li>
<li>testPostService</li>
<li>testPostRepository</li>
</ul>
<hr>
<h2 id="7-applicationcontext-동적-빈-검색">7. ApplicationContext 동적 빈 검색</h2>
<p>이제 ApplicationContext는 더 이상 하드코딩 방식이 아니라
패키지를 스캔해서 동적으로 빈을 찾도록 변경했다.</p>
<pre><code class="language-java">applicationContext.genBean(&quot;testPostService&quot;);</code></pre>
<p>내부적으로는 스캔된 클래스 목록에서
빈 이름에 맞는 클래스를 찾아 객체를 생성한다.</p>
<hr>
<h2 id="8-댓글-삭제-api-구현">8. 댓글 삭제 API 구현</h2>
<p>REST API에서 댓글 삭제 기능을 구현했다.</p>
<pre><code class="language-text">/api/v1/posts/1/comments/2/delete</code></pre>
<p>처음에는 개발 편의를 위해 GET 방식으로 호출할 수 있게 만들었다.</p>
<p>하지만 삭제는 서버 상태를 변경하는 작업이므로
원칙적으로는 GET이 아니라 DELETE 메서드를 사용하는 것이 맞다.</p>
<hr>
<h2 id="9-orphanremoval과-더티체킹">9. orphanRemoval과 더티체킹</h2>
<p>댓글 삭제 과정에서 <code>orphanRemoval = true</code>와 더티체킹을 활용했다.</p>
<pre><code class="language-java">@OneToMany(mappedBy = &quot;post&quot;, orphanRemoval = true)
private List&lt;PostComment&gt; comments;</code></pre>
<p>부모 엔티티에서 자식 엔티티를 제거하면
JPA가 변경을 감지해서 삭제 쿼리를 실행한다.</p>
<p>단, 더티체킹은 트랜잭션 안에서 동작하므로
<code>@Transactional</code>이 필요하다.</p>
<hr>
<h2 id="10-조회-메서드-readonly-트랜잭션">10. 조회 메서드 readOnly 트랜잭션</h2>
<p>조회 전용 API에는 <code>readOnly = true</code> 옵션을 적용했다.</p>
<pre><code class="language-java">@Transactional(readOnly = true)</code></pre>
<p>조회 작업임을 명확히 표현할 수 있고,
불필요한 변경 감지를 줄일 수 있다.</p>
<hr>
<h2 id="11-resultcode와-msg">11. resultCode와 msg</h2>
<p>조회가 아닌 요청에는
처리 결과를 명확하게 알려주는 응답이 필요하다.</p>
<pre><code class="language-json">{
  &quot;resultCode&quot;: &quot;200-1&quot;,
  &quot;msg&quot;: &quot;댓글이 삭제되었습니다.&quot;
}</code></pre>
<p><code>resultCode</code>는 HTTP 상태코드와 세부 구분코드를 함께 표현한다.</p>
<pre><code class="language-text">200-1</code></pre>
<ul>
<li>200: 성공</li>
<li>-1: 세부 구분 코드</li>
</ul>
<hr>
<h2 id="12-rsdata-클래스-도입">12. RsData 클래스 도입</h2>
<p>조회 API를 제외한 요청 응답에 사용할
공통 응답 객체를 만들었다.</p>
<pre><code class="language-java">public class RsData&lt;T&gt; {
    private String resultCode;
    private String msg;
    private T data;
}</code></pre>
<p>사용 예시:</p>
<ul>
<li>글 작성 응답</li>
<li>글 수정 응답</li>
<li>글 삭제 응답</li>
<li>댓글 작성 응답</li>
<li>댓글 삭제 응답</li>
</ul>
<hr>
<h2 id="13-data-필드-추가">13. data 필드 추가</h2>
<p><code>resultCode</code>와 <code>msg</code>만으로 부족한 경우를 위해
<code>data</code> 필드를 추가했다.</p>
<pre><code class="language-java">RsData&lt;PostDto&gt;</code></pre>
<p>응답 결과와 함께
추가 데이터를 담을 수 있게 되었다.</p>
<hr>
<h2 id="14-rsdata-제네릭-적용">14. RsData 제네릭 적용</h2>
<p>처음에는 <code>data</code> 타입을 <code>Object</code>로 처리했지만,
이후 제네릭을 적용했다.</p>
<pre><code class="language-java">RsData&lt;PostDto&gt;
RsData&lt;PostCommentDto&gt;
RsData&lt;Void&gt;</code></pre>
<p>제네릭을 사용하면
응답 데이터 타입을 더 명확하게 표현할 수 있다.</p>
<hr>
<h2 id="15-rsdatavoid">15. RsData<Void></h2>
<p>응답에 추가 데이터가 필요 없는 경우에는
<code>RsData&lt;Void&gt;</code>를 사용한다.</p>
<pre><code class="language-java">RsData&lt;Void&gt;</code></pre>
<p>데이터 없이 resultCode와 msg만 전달할 때 적합하다.</p>
<hr>
<h2 id="16-글-삭제-api-구현">16. 글 삭제 API 구현</h2>
<p>글 삭제 기능도 구현했다.</p>
<pre><code class="language-text">/api/v1/posts/1/delete</code></pre>
<p>글 삭제 시 관련 댓글은
<code>CascadeType.REMOVE</code> 설정 덕분에 함께 삭제된다.</p>
<hr>
<h2 id="17-postman-사용">17. Postman 사용</h2>
<p>Postman을 사용해 REST API를 직접 호출하고 테스트했다.</p>
<p>예시 요청:</p>
<pre><code class="language-text">GET /api/v1/posts
GET /api/v1/posts/2
GET /api/v1/posts/2/comments
GET /api/v1/posts/2/comments/4</code></pre>
<p>Postman을 사용하면
브라우저로 호출하기 어려운 DELETE, POST 요청도 쉽게 테스트할 수 있다.</p>
<hr>
<h2 id="18-delete-메서드-적용">18. DELETE 메서드 적용</h2>
<p>삭제 요청을 GET에서 DELETE 방식으로 변경했다.</p>
<p>기존:</p>
<pre><code class="language-text">GET /api/v1/posts/1/delete</code></pre>
<p>변경:</p>
<pre><code class="language-text">DELETE /api/v1/posts/1</code></pre>
<p>REST API에서는 삭제 작업에 DELETE 메서드를 사용하는 것이 더 자연스럽다.</p>
<hr>
<h2 id="19-post-요청과-requestbody">19. POST 요청과 @RequestBody</h2>
<p>글 작성 API는 POST 요청으로 처리하고,
요청 본문은 JSON 형태로 받도록 변경했다.</p>
<pre><code class="language-json">{
  &quot;title&quot;: &quot;제목&quot;,
  &quot;content&quot;: &quot;내용&quot;
}</code></pre>
<pre><code class="language-java">@PostMapping
public RsData&lt;PostDto&gt; write(@RequestBody PostWriteReqBody reqBody) {
}</code></pre>
<p><code>@RequestBody</code>는 JSON 요청 본문을 자바 객체로 변환해준다.</p>
<hr>
<h2 id="20-rest-api와-bindingresult">20. REST API와 BindingResult</h2>
<p>REST API에서는 <code>@Valid</code>를 사용하더라도
BindingResult를 직접 사용하는 경우가 많지 않다.</p>
<p>폼 기반 서버 렌더링에서는 검증 실패 후
다시 폼 화면을 보여줘야 하지만,
REST API에서는 보통 에러 응답을 JSON으로 내려준다.</p>
<hr>
<h2 id="21-reqbody-네이밍">21. ReqBody 네이밍</h2>
<p>기존 <code>PostWriteForm</code>이라는 이름을
<code>PostWriteReqBody</code>로 변경했다.</p>
<pre><code class="language-java">public record PostWriteReqBody(
    String title,
    String content
) {
}</code></pre>
<p>REST API에서는 폼보다는 요청 본문을 의미하는
<code>ReqBody</code>라는 이름이 더 정확하다.</p>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>Reflections 라이브러리를 사용해 @Component 계열 클래스를 동적으로 수집할 수 있었다.</li>
<li>ApplicationContext에서 하드코딩을 줄이고 패키지 스캔 기반 빈 생성을 구현할 수 있었다.</li>
<li>댓글 삭제에서는 orphanRemoval과 더티체킹, 트랜잭션의 관계를 이해할 수 있었다.</li>
<li>RsData를 도입하면서 REST API 응답 구조를 공통화할 수 있었다.</li>
<li>DELETE, POST, @RequestBody를 사용하면서 REST API다운 요청 구조에 가까워졌다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 54 - Rest API]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-54-Rest-API</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-54-Rest-API</guid>
            <pubDate>Wed, 20 May 2026 07:39:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 05월 19일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 REST API 기반 프로젝트 세팅과
JPA, DTO, API 버저닝, 댓글 API 구현까지 정리했다.</p>
<hr>
<h2 id="1-rest-api-프로젝트-세팅">1. REST API 프로젝트 세팅</h2>
<pre><code class="language-kotlin">implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)</code></pre>
<p>REST API 서버 개발에 필요한 기본 의존성을 추가했다.</p>
<hr>
<h2 id="2-rest-api-vs-타임리프">2. REST API vs 타임리프</h2>
<p>REST API 방식에서는
백엔드는 데이터(JSON)만 제공한다.</p>
<pre><code class="language-json">{
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;제목 1&quot;,
  &quot;content&quot;: &quot;내용 1&quot;
}</code></pre>
<p>타임리프 방식은 HTML까지 서버에서 렌더링해서 응답한다.</p>
<hr>
<h2 id="3-post-도메인-구성">3. Post 도메인 구성</h2>
<ul>
<li>JPA</li>
<li>BaseEntity</li>
<li>InitData</li>
<li>Profile 설정</li>
</ul>
<p>등 게시글 도메인의 기본 구조를 구성했다.</p>
<hr>
<h2 id="4-게시글-샘플-데이터-생성">4. 게시글 샘플 데이터 생성</h2>
<pre><code class="language-java">postService.write(&quot;제목 1&quot;, &quot;내용 1&quot;);</code></pre>
<p>게시글 3개를 자동 생성하도록 구성했다.</p>
<hr>
<h2 id="5-댓글-도메인-추가">5. 댓글 도메인 추가</h2>
<pre><code class="language-java">postCommentService.write(post, &quot;댓글 1&quot;);</code></pre>
<p>댓글 기능과 샘플 데이터 5개를 추가했다.</p>
<hr>
<h2 id="6-글-다건조회-api">6. 글 다건조회 API</h2>
<pre><code class="language-java">@GetMapping
public List&lt;Post&gt; getPosts() {
    return postService.findAll();
}</code></pre>
<p>엔드포인트:</p>
<pre><code class="language-text">/api/v1/posts</code></pre>
<hr>
<h2 id="7-순환참조-문제">7. 순환참조 문제</h2>
<p>양방향 연관관계에서는
JSON 직렬화 시 무한 루프가 발생할 수 있다.</p>
<hr>
<h2 id="8-jsonignore">8. @JsonIgnore</h2>
<pre><code class="language-java">@JsonIgnore
private Post post;</code></pre>
<p>순환참조를 방지하기 위해 적용했다.</p>
<hr>
<h2 id="9-restcontroller">9. @RestController</h2>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api/v1/posts&quot;)</code></pre>
<p><code>@RestController</code>는
JSON 응답을 위한 컨트롤러이다.</p>
<hr>
<h2 id="10-글-단건조회-api">10. 글 단건조회 API</h2>
<pre><code class="language-java">@GetMapping(&quot;/{id}&quot;)
public Post getPost(@PathVariable long id)</code></pre>
<p>엔드포인트:</p>
<pre><code class="language-text">/api/v1/posts/3</code></pre>
<hr>
<h2 id="11-jackson">11. Jackson</h2>
<p>Jackson은 Java 객체와 JSON 간 변환을 담당한다.</p>
<p>기능:</p>
<ul>
<li>객체 → JSON</li>
<li>JSON → 객체</li>
</ul>
<hr>
<h2 id="12-rest-api-vs-타임리프-비교">12. REST API vs 타임리프 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>REST API</th>
<th>타임리프</th>
</tr>
</thead>
<tbody><tr>
<td>응답</td>
<td>JSON</td>
<td>HTML</td>
</tr>
<tr>
<td>화면 처리</td>
<td>프론트</td>
<td>서버</td>
</tr>
<tr>
<td>재사용성</td>
<td>높음</td>
<td>낮음</td>
</tr>
</tbody></table>
<hr>
<h2 id="13-api-버저닝">13. API 버저닝</h2>
<pre><code class="language-text">/api/v1/posts</code></pre>
<p>REST API에서는 버저닝을 통해
하위 호환성을 유지한다.</p>
<hr>
<h2 id="14-엔티티-직접-노출-문제">14. 엔티티 직접 노출 문제</h2>
<p>프론트 요구사항 때문에
엔티티 필드를 직접 수정하는 것은 좋지 않다.</p>
<p>예시:</p>
<ul>
<li>createDate → createdDate</li>
<li>title → subject</li>
<li>content → body</li>
</ul>
<hr>
<h2 id="15-dto-도입">15. DTO 도입</h2>
<pre><code class="language-java">public record PostDto(
    long id,
    String title,
    String content
) {
}</code></pre>
<p>엔티티 대신 DTO를 응답 객체로 사용했다.</p>
<hr>
<h2 id="16-record-사용">16. record 사용</h2>
<pre><code class="language-java">.map(PostDto::new)</code></pre>
<p>record 기반 DTO 구조로 변경했다.</p>
<hr>
<h2 id="17-댓글-다건조회-api">17. 댓글 다건조회 API</h2>
<pre><code class="language-text">/api/v1/comments</code></pre>
<p>댓글 전체 조회 기능을 구현했다.</p>
<hr>
<h2 id="18-댓글-단건조회-api">18. 댓글 단건조회 API</h2>
<pre><code class="language-text">/api/v1/posts/1/comments/2</code></pre>
<p>특정 게시글의 댓글 단건 조회를 구현했다.</p>
<hr>
<h2 id="19-평면-리소스-vs-중첩-리소스">19. 평면 리소스 vs 중첩 리소스</h2>
<table>
<thead>
<tr>
<th>방식</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>평면 리소스</td>
<td>/api/v1/comments</td>
</tr>
<tr>
<td>중첩 리소스</td>
<td>/api/v1/posts/1/comments</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>REST API 프로젝트를 구성하면서 JSON 기반 응답 구조를 이해할 수 있었다.</li>
<li>양방향 연관관계에서는 순환참조 문제가 발생할 수 있으며 @JsonIgnore로 해결할 수 있다.</li>
<li>DTO를 사용하면 엔티티와 API 응답 구조를 분리할 수 있다.</li>
<li>API 버저닝을 통해 하위 호환성을 유지하면서 API를 확장할 수 있다.</li>
<li>댓글 API까지 추가하면서 게시글과 댓글 간 관계 구조를 구현할 수 있었다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 53 - Interceptor]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-53-Interceptor-fl7fhemb</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-53-Interceptor-fl7fhemb</guid>
            <pubDate>Mon, 18 May 2026 08:29:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 05월 18일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 인터셉터 기반 인증/인가 처리와
스코프(scope), 세션, request 객체 활용까지 정리했다.</p>
<hr>
<h2 id="1-인터셉터-프로젝트-세팅">1. 인터셉터 프로젝트 세팅</h2>
<pre><code class="language-gradle">implementation(&quot;org.springframework.boot:spring-boot-starter-thymeleaf&quot;)</code></pre>
<p>Spring MVC 기반 웹 프로젝트 환경을 구성했다.</p>
<hr>
<h2 id="2-인터셉터-등록">2. 인터셉터 등록</h2>
<pre><code class="language-java">registry.addInterceptor(new LoginInterceptor());</code></pre>
<p>인터셉터를 등록하고 요청 흐름에서 로그를 확인했다.</p>
<hr>
<h2 id="3-로그인-체크-인터셉터">3. 로그인 체크 인터셉터</h2>
<pre><code class="language-java">public class LoginCheckInterceptor implements HandlerInterceptor {
}</code></pre>
<p>로그인 여부를 검사하는 인터셉터를 추가했다.</p>
<hr>
<h2 id="4-로그인-폼-구현">4. 로그인 폼 구현</h2>
<pre><code class="language-html">&lt;form method=&quot;post&quot;&gt;
    &lt;input type=&quot;text&quot; name=&quot;username&quot;&gt;
    &lt;input type=&quot;password&quot; name=&quot;password&quot;&gt;
&lt;/form&gt;</code></pre>
<hr>
<h2 id="5-로그인--로그아웃-처리">5. 로그인 / 로그아웃 처리</h2>
<pre><code class="language-java">session.setAttribute(&quot;loginedMember&quot;, member);</code></pre>
<pre><code class="language-java">session.invalidate();</code></pre>
<p>세션 기반 로그인/로그아웃을 구현했다.</p>
<hr>
<h2 id="6-로그인-기능-보완">6. 로그인 기능 보완</h2>
<ul>
<li>로그인 실패 처리</li>
<li>세션 유지</li>
<li>리다이렉트 흐름 개선</li>
</ul>
<p>등 로그인 흐름을 보완했다.</p>
<hr>
<h2 id="7-싱글톤-스코프">7. 싱글톤 스코프</h2>
<pre><code class="language-java">@Scope(&quot;singleton&quot;)</code></pre>
<p>기본적으로 스프링 빈은 싱글톤 스코프를 사용한다.</p>
<p>특징:</p>
<ul>
<li>컨테이너당 1개</li>
<li>메모리 효율적</li>
<li>상태 공유 주의 필요</li>
</ul>
<hr>
<h2 id="8-세션-스코프">8. 세션 스코프</h2>
<pre><code class="language-java">@SessionScope</code></pre>
<p>브라우저(HTTP 세션)마다
별도의 객체가 생성된다.</p>
<hr>
<h2 id="9-세션-스코프와-프록시">9. 세션 스코프와 프록시</h2>
<pre><code class="language-java">@SessionScope(proxyMode = ScopedProxyMode.TARGET_CLASS)</code></pre>
<p>세션 스코프 빈은 내부적으로 프록시 객체를 사용한다.</p>
<hr>
<h2 id="10-세션에-로그인-정보-저장">10. 세션에 로그인 정보 저장</h2>
<pre><code class="language-java">session.setAttribute(&quot;member&quot;, member);</code></pre>
<p>사용자별 로그인 상태를 유지할 수 있게 되었다.</p>
<hr>
<h2 id="11-request-스코프">11. Request 스코프</h2>
<pre><code class="language-java">@RequestScope
public class Rq {
}</code></pre>
<p>Rq 객체를 request 스코프로 변경했다.</p>
<hr>
<h2 id="12-request-스코프-특징">12. Request 스코프 특징</h2>
<p>request 스코프 객체는
하나의 요청 안에서 데이터를 공유하기 좋다.</p>
<p>활용 예시:</p>
<ul>
<li>인터셉터</li>
<li>컨트롤러</li>
<li>템플릿</li>
</ul>
<hr>
<h2 id="13-현재-url-조회">13. 현재 URL 조회</h2>
<pre><code class="language-java">rq.getCurrentUrl();</code></pre>
<p>현재 요청 주소를 쉽게 가져올 수 있도록 개선했다.</p>
<hr>
<h2 id="14-rq에-로그인-정보-저장">14. rq에 로그인 정보 저장</h2>
<pre><code class="language-java">rq.setLoginedMember(member);</code></pre>
<p>rq 객체에서 로그인 정보를 직접 관리하도록 변경했다.</p>
<hr>
<h2 id="15-rq-기반-코드-개선">15. rq 기반 코드 개선</h2>
<p>rq 중심 구조로 리팩토링하면서</p>
<ul>
<li>중복 제거</li>
<li>로그인 처리 통합</li>
<li>코드 가독성 향상</li>
</ul>
<p>효과를 얻을 수 있었다.</p>
<hr>
<h2 id="16-rq에서-member-객체-직접-관리">16. rq에서 Member 객체 직접 관리</h2>
<p>기존:</p>
<pre><code class="language-java">Integer memberId</code></pre>
<p>개선:</p>
<pre><code class="language-java">Member member</code></pre>
<p>객체 기반 접근으로 코드가 더 직관적으로 바뀌었다.</p>
<hr>
<h2 id="17-스코프-종류-정리">17. 스코프 종류 정리</h2>
<table>
<thead>
<tr>
<th>스코프</th>
<th>범위</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>singleton</td>
<td>컨테이너당 1개</td>
<td>기본 스코프</td>
</tr>
<tr>
<td>prototype</td>
<td>요청마다 새 객체</td>
<td>상태 독립</td>
</tr>
<tr>
<td>application</td>
<td>ServletContext 단위</td>
<td>웹 전역</td>
</tr>
<tr>
<td>session</td>
<td>세션당 1개</td>
<td>사용자별 상태</td>
</tr>
<tr>
<td>request</td>
<td>요청당 1개</td>
<td>요청 임시 데이터</td>
</tr>
</tbody></table>
<hr>
<h2 id="18-singleton-vs-prototype">18. Singleton vs Prototype</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>Singleton</th>
<th>Prototype</th>
</tr>
</thead>
<tbody><tr>
<td>생성 횟수</td>
<td>1개</td>
<td>매번 생성</td>
</tr>
<tr>
<td>메모리 사용</td>
<td>적음</td>
<td>많음</td>
</tr>
<tr>
<td>상태 공유</td>
<td>O</td>
<td>X</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>인터셉터를 사용하면 공통 인증/인가 처리를 효율적으로 관리할 수 있다.</li>
<li>세션을 이용하면 사용자별 로그인 상태를 유지할 수 있다.</li>
<li>세션 스코프와 request 스코프를 통해 상황에 맞는 객체 생명주기를 사용할 수 있다.</li>
<li>request 스코프 객체는 요청 단위 데이터 공유에 특히 유용하다.</li>
<li>rq 객체를 중심으로 로그인 정보를 관리하면서 코드 구조를 더 깔끔하게 정리할 수 있었다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 52 - Mapping]]></title>
            <link>https://velog.io/@xxxhyxxk__/Day-52-Mapping-fpl7tw98</link>
            <guid>https://velog.io/@xxxhyxxk__/Day-52-Mapping-fpl7tw98</guid>
            <pubDate>Fri, 15 May 2026 10:33:29 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 2026년 05월 15일 작성된 글입니다.</p>
</blockquote>
<p>오늘은 생성자 파라미터 분석 유틸, 회원 CRUD 및 검색,
그리고 MyBatis resultMap과 association/collection 매핑까지 정리했다.</p>
<hr>
<h2 id="1-생성자-파라미터-정보-조회">1. 생성자 파라미터 정보 조회</h2>
<pre><code class="language-java">Parameter[] parameters =
    ClsUtil.getParameters(
        TestCar.class,
        new Object[]{&quot;BMW&quot;, 1234}
    );</code></pre>
<p>생성자의 타입과 이름 정보를 조회할 수 있도록 구현했다.</p>
<hr>
<h2 id="2-생성자-파라미터-이름-조회">2. 생성자 파라미터 이름 조회</h2>
<pre><code class="language-java">String[] parameterNames =
    ClsUtil.getParameterNames(
        TestCar.class,
        new Object[]{&quot;BMW&quot;, 1234}
    );</code></pre>
<p>Reflection 기반으로 생성자 파라미터 이름을 추출했다.</p>
<hr>
<h2 id="3-타입-기반-생성자-선택">3. 타입 기반 생성자 선택</h2>
<p>여러 생성자가 있을 때
입력된 타입과 일치하는 생성자를 선택하도록 개선했다.</p>
<ul>
<li>String</li>
<li>int</li>
</ul>
<p>등 타입 기준 생성자 탐색 가능.</p>
<hr>
<h2 id="4-함수-파라미터-타입-조회-개선">4. 함수 파라미터 타입 조회 개선</h2>
<p>특정 함수의 파라미터를
타입 기준으로 조회할 수 있도록 개선했다.</p>
<hr>
<h2 id="5-회원-단건-조회">5. 회원 단건 조회</h2>
<pre><code class="language-java">Member member = memberService.findByUsername(&quot;user2&quot;);</code></pre>
<p>username 기반 회원 조회 테스트를 구현했다.</p>
<hr>
<h2 id="6-회원-생성">6. 회원 생성</h2>
<pre><code class="language-java">int id = memberService.create(
    &quot;user3&quot;,
    &quot;{noop}1234&quot;,
    &quot;유저3&quot;,
    &quot;user3@test.com&quot;
);</code></pre>
<p>회원 생성 후 다시 조회해서 저장 여부를 검증했다.</p>
<hr>
<h2 id="7-회원-삭제">7. 회원 삭제</h2>
<pre><code class="language-java">memberService.deleteById(1);</code></pre>
<p>삭제 후 회원 개수를 확인했다.</p>
<hr>
<h2 id="8-회원-수정">8. 회원 수정</h2>
<pre><code class="language-java">memberService.update(
    1,
    &quot;user1_updated&quot;,
    &quot;{noop}5678&quot;,
    &quot;유저1 수정&quot;,
    &quot;user1_updated@test.com&quot;
);</code></pre>
<p>회원 정보 수정 기능을 구현했다.</p>
<hr>
<h2 id="9-일부-데이터만-수정">9. 일부 데이터만 수정</h2>
<pre><code class="language-java">memberService.update(
    1,
    &quot;&quot;,
    &quot;&quot;,
    &quot;유저1 수정됨&quot;,
    &quot;&quot;
);</code></pre>
<p>빈 값은 유지하고 특정 필드만 수정 가능하도록 처리했다.</p>
<hr>
<h2 id="10-회원-검색">10. 회원 검색</h2>
<pre><code class="language-java">memberService.search(&quot;username&quot;, &quot;user1&quot;);</code></pre>
<p>검색 기준:</p>
<ul>
<li>username</li>
<li>name</li>
<li>email</li>
<li>전체 검색</li>
</ul>
<hr>
<h2 id="11-다중-회원-삭제">11. 다중 회원 삭제</h2>
<pre><code class="language-java">memberService.deleteByIds(Arrays.asList(id3, id4));</code></pre>
<p>foreach 기반 다중 삭제 기능을 구현했다.</p>
<hr>
<h2 id="12-게시글-작성자-정보-추가">12. 게시글 작성자 정보 추가</h2>
<pre><code class="language-sql">ALTER TABLE post
ADD COLUMN memberId BIGINT UNSIGNED NULL;</code></pre>
<p>게시글과 회원을 연결할 수 있도록 memberId를 추가했다.</p>
<hr>
<h2 id="13-작성자-이름-포함-조회">13. 작성자 이름 포함 조회</h2>
<pre><code class="language-java">Post post = postService.findByIdWithAuthorName(1);</code></pre>
<p>JOIN 기반으로 작성자 이름까지 함께 조회했다.</p>
<hr>
<h2 id="14-작성자-이름-검색">14. 작성자 이름 검색</h2>
<pre><code class="language-java">List&lt;Post&gt; posts =
    postService.searchWithAuthorName(
        &quot;author&quot;,
        &quot;유저1&quot;
    );</code></pre>
<p>작성자 이름 기반 검색 기능을 구현했다.</p>
<hr>
<h2 id="15-resultmap">15. resultMap</h2>
<pre><code class="language-xml">&lt;resultMap id=&quot;PostWithAuthorMap&quot; type=&quot;Post&quot;&gt;</code></pre>
<p>JOIN 결과를 객체 구조에 맞게 매핑할 수 있다.</p>
<hr>
<h2 id="16-association">16. association</h2>
<pre><code class="language-xml">&lt;association property=&quot;author&quot;
             javaType=&quot;Member&quot;&gt;</code></pre>
<p>association은 1:1 관계 매핑에 사용된다.</p>
<p>예시:</p>
<ul>
<li>게시글 → 작성자</li>
<li>회원 → 프로필</li>
</ul>
<hr>
<h2 id="17-collection">17. collection</h2>
<pre><code class="language-xml">&lt;collection property=&quot;comments&quot;
            ofType=&quot;Comment&quot;&gt;</code></pre>
<p>collection은 1:N 관계 매핑에 사용된다.</p>
<p>예시:</p>
<ul>
<li>게시글 → 댓글 목록</li>
<li>주문 → 주문 항목 목록</li>
</ul>
<hr>
<h2 id="18-association-vs-collection">18. association vs collection</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>association</th>
<th>collection</th>
</tr>
</thead>
<tbody><tr>
<td>관계</td>
<td>1:1</td>
<td>1:N</td>
</tr>
<tr>
<td>타입</td>
<td>단일 객체</td>
<td>리스트</td>
</tr>
<tr>
<td>태그</td>
<td><code>&lt;association&gt;</code></td>
<td><code>&lt;collection&gt;</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<ul>
<li>Reflection 기반으로 생성자 파라미터 정보를 조회하는 기능을 구현할 수 있었다.</li>
<li>회원 CRUD와 검색 기능을 구현하면서 서비스 구조를 확장할 수 있었다.</li>
<li>게시글과 회원을 연결하여 작성자 개념을 추가할 수 있었다.</li>
<li>MyBatis resultMap을 사용하면 JOIN 결과를 객체 구조에 맞게 유연하게 매핑할 수 있다.</li>
<li>association은 1:1 관계, collection은 1:N 관계 매핑에 사용된다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>