<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>se_kite.log</title>
        <link>https://velog.io/</link>
        <description>조금씩 매일 성장하자</description>
        <lastBuildDate>Wed, 16 Jul 2025 09:59:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>se_kite.log</title>
            <url>https://velog.velcdn.com/images/se_kite/profile/ebf66e67-9b8b-489d-9101-ff5d9a22d1a8/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. se_kite.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/se_kite" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Lock wait timeout exceed 에러 상황 재현]]></title>
            <link>https://velog.io/@se_kite/Lock-wait-timeout-exceed-%EC%97%90%EB%9F%AC-%EC%83%81%ED%99%A9-%EC%9E%AC%ED%98%84</link>
            <guid>https://velog.io/@se_kite/Lock-wait-timeout-exceed-%EC%97%90%EB%9F%AC-%EC%83%81%ED%99%A9-%EC%9E%AC%ED%98%84</guid>
            <pubDate>Wed, 16 Jul 2025 09:59:37 GMT</pubDate>
            <description><![CDATA[<p>Lock wait timeout exceeded 에러가 발생하는 대표적인 상황을 MySQL에서 직접 재현해봤다.</p>
<h2 id="전체-조건">전체 조건</h2>
<ul>
<li>MySQL 서버에서 InnoDB 스토리지 엔진 사용</li>
<li>innodb_lock_wait_timeout: 50초(기본값)</li>
<li>두 개의 터미널/세션을 사용해 실행</li>
</ul>
<h2 id="테스트-과정">테스트 과정</h2>
<h3 id="1-테스트-테이블-생성">1. 테스트 테이블 생성</h3>
<pre><code class="language-sql">CREATE TABLE accounts (
    id INT PRIMARY KEY,
    balance INT
) ENGINE=InnoDB;

INSERT INTO accounts VALUES (1, 100), (2, 100);</code></pre>
<h3 id="2-session-a-트랜잭션-시작-후-레코드-잠금">2. Session A: 트랜잭션 시작 후 레코드 잠금</h3>
<pre><code class="language-sql">START TRANSACTION;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
-- 여기서 COMMIT 하지 말고 대기!</code></pre>
<ul>
<li>id = 1 행에 쓰기 잠금 (exclusive lock) 이 걸린다.</li>
<li>트랜잭션을 커밋하지 않았기 때문에 락은 유지된다.</li>
</ul>
<h3 id="3-session-b-같은-레코드에-접근-시도">3. Session B: 같은 레코드에 접근 시도</h3>
<pre><code class="language-sql">START TRANSACTION;
UPDATE accounts SET balance = balance + 20 WHERE id = 1;</code></pre>
<ul>
<li>id = 1은 이미 Session A에서 락을 걸고 있으므로, Session B는 락 해제를 기다리며 대기 상태에 빠진다.</li>
</ul>
<h3 id="4-일정-시간-후-lock-wait-timeout-exceed-에러-발생">4. 일정 시간 후 <code>Lock wait timeout exceed</code> 에러 발생</h3>
<p><img src="https://i.postimg.cc/Kz00pP0X/image.png" alt="에러 발생"></p>
<ul>
<li>기본 설정 기준 50초 이상 대기하면 위와 같은 에러가 발생한다.</li>
</ul>
<hr>
<h2 id="문제-발생-원인">문제 발생 원인</h2>
<p>원인) Session A가 UPDATE를 실행한 후 트랜잭션을 종료하지 않았기 때문</p>
<ul>
<li>트랜잭션이 종료되지 않으면 락은 계속 유지되며, 다른 트랜잭션은 해당 레코드에 접근할 수 없다.</li>
<li>Session B는 락을 얻기 위해 기다리다 해당 에러가 발생했다.</li>
</ul>
<h2 id="해결방법">해결방법</h2>
<p>Session A 에서 빠르게 커밋하기</p>
<pre><code class="language-sql">COMMIT;</code></pre>
<p>커밋 → 락 해제 → Session B는 update 쿼리 실행 가능</p>
<blockquote>
<p>🚨 명시적으로 트랜잭션을 시작했을 때(<code>START TRANSACTION</code>)을 사용했을 때는 
<strong>“트랜잭션의 끝을 명확히 닫아주는 것”</strong>이 매우 중요하다. 
그렇지 않으면 락이 유지되며 다른 트랜잭션이 블로킹되거나 타임아웃으로 이어질 수 있다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 데브코스 커리어 TALK] 참여 후기]]></title>
            <link>https://velog.io/@se_kite/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-%EC%BB%A4%EB%A6%AC%EC%96%B4-TALK-%EC%B0%B8%EC%97%AC-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@se_kite/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-%EC%BB%A4%EB%A6%AC%EC%96%B4-TALK-%EC%B0%B8%EC%97%AC-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 07 Jul 2025 11:22:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/se_kite/post/f07c73de-95c8-43a7-967e-be4da8f02f75/image.png" alt=""></p>
<h2 id="1부-취업난-속에서도-살아남는-신입-개발자의-전략-멘토링-현장에서-찾은-생존-팁">1부. 취업난 속에서도 살아남는 신입 개발자의 전략: 멘토링 현장에서 찾은 생존 팁</h2>
<blockquote>
<h3 id="취업--실력--태도--운-여기서-실력은-어떻게-기를-수-있을까">&quot;취업 = 실력 + 태도 + 운&quot; 여기서 실력은 어떻게 기를 수 있을까?</h3>
</blockquote>
<p>실력은 <strong>개발 지식 + 개발 지식의 적용</strong>이다.
여기서 말하는 <strong>개발 지식은 CS 공부</strong>를 말하고 <strong>적용은 이를 코드에 적용</strong>하는 것이다. </p>
<p>ex. 
인덱스 : explain 살펴보기 , 1000만개 이상 데이터 넣고 인덱스 걸기 전/후 파악 
AOP: 트랜잭션 update했을때 commit을 하는데 그 전에 다른 호출을 하면 값이 달라짐(어노테이션의 구성, 동작원리 코드로 이해하기)</p>
<ul>
<li>용어와 개념은 제대로 이해하고 이를 정의할 줄 알아야한다.</li>
<li>기술을 배울때는 해당 기술이 어떤 목적으로 생겼으며 어떤 과거 기술이 영향을 받았는지 연관지어 공부해야한다.</li>
<li>모든 기술에는 장/단점이 있다. 이를 파악해둬야 나중에 어떤 기술을 사용할때 선택의 기준이 된다.
→ 연구자는 1%라도 개선의 가능성이 있으면 연구하지만 엔지니어는 비용과 효율을 생각해야하기 때문에 장단점을 알고 있어야한다. </li>
<li>CS 공부에 왕도는 없다. 절대적인 시간을 쏟아부을 수 밖에 없다.</li>
</ul>
<blockquote>
<h3 id="그럼-태도는-무엇인가-일을-어떻게-대하는가바라보는가이다">그럼 태도는 무엇인가? 일을 어떻게 대하는가/바라보는가이다.</h3>
</blockquote>
<p>그렇다면 우리는 태도를 어떻게 면접관에게 알릴 수 있을까?</p>
<ul>
<li>포르폴리오( 깃헙을 첨부하면 코드를 꼭 본다⭐)</li>
<li>소규모(10-20명의 유저)서비스를 개발하고 운영해본 경험
이때, 가벼운 배포 / 도메인 구입 / aws 사용 / 홍보 등 적은 비용에 제한된 환경에서 개발해본 경험이 좋다.</li>
<li>사용자의 요구사항이나 리뷰를 반영해본 부분을 블로그에 기록하기⭐</li>
</ul>
<hr>
<h2 id="2부-데브코스-현업까지-fe-수료생이-들려주는-실전-취업-사례와-전략">2부. 데브코스 현업까지: FE 수료생이 들려주는 실전 취업 사례와 전략</h2>
</br>

<h3 id="이력서-tip">이력서 Tip)</h3>
<ul>
<li>본인을 소개하는 3~4 문장은 머릿말에 작성하지 말고 마지막에 작성하자
→ 이력서를 쭉 읽고 요약 소개처럼 느껴지는게 point!</li>
<li>객관적인 자료를 확인할 수 있는 정략적인 수치와 지표 사용하기</li>
<li>성능개선 전/후와 같은 기술 외적으로도 어필 요소가 있다면 작성하기
ex. PR 규칙 적용을 통해 협업 능력 기여 등</li>
</ul>
<h3 id="회사-지원-tip">회사 지원 Tip)</h3>
<ul>
<li>하향 지원 → 상향 지원 → 적정 지원</li>
<li>3년차 공고에도 지원해보기
→ 신입 지원인 걸 염두하고 질문하니 너무 걱정말고 일단 지원해보자!</li>
</ul>
<h3 id="면접-준비-tip">면접 준비 Tip)</h3>
<ul>
<li>AI를 활용해 이력서/포폴 위주 예상 질문에 답변하는 연습하기</li>
<li>모집 공고에 작성되어 있는 기술 스택 준비하기</li>
<li>질문에 대한 대답 후, 한줄 요약으로 정리해서 다시 말하기</li>
<li>비대면 면접이라면 컨닝하지 말자! 눈이 움직이는거 다~~ 보인다.</li>
<li>면접 끝나면 꼭! 질문 복기하기</li>
</ul>
<h3 id="포폴-tip">포폴 Tip)</h3>
<ul>
<li>블로그에 게시글 작성해 활용하자. 이떄, 기록이 초점이 아니라 자신만의 언어로 남기는게 중요하다
ex. 코테 접근 방식이나 문제풀이 해설, CS 공부, 프로젝트 유지보수한 과정, 회고 등</li>
<li>특히, 프로젝트 리팩토링하는 경우 전/후를 문서화해서 성능 개선이나 자동화 적용 등을 하면 좋다. </li>
</ul>
</br>


<p><em>무엇보다 가장 중요한 건! 
취준은 장기전이라 번아웃이 오지 않게 자신의 페이스로 준비하자 + 운동</em></p>
<hr>
<h2 id="3부-현업-개발자가-직접-알려주는-내가-함께-일하고-싶은-신입-개발자">3부. 현업 개발자가 직접 알려주는 &quot;내가 함께 일하고 싶은 신입 개발자&quot;</h2>
<h3 id="1-호기심을-뿜어내는-동료">1. 호기심을 뿜어내는 동료</h3>
<p>분기별, 매년 회고를 통해 나는 어떤일을 하고있는지, 하는일이 어떤 방향성을 갖는지 생각해보는 시간 갖기</p>
<h3 id="2-주변에-관심를-많이-가지는-동료">2. 주변에 관심를 많이 가지는 동료</h3>
<p>주변에 관심이 많으면 결국 자신의 성장으로 돌아온다. 작은 행동이 스노우볼처럼 되돌아올 수 있다.</p>
<h3 id="3-질문을-잘-하는-동료">3. 질문을 잘 하는 동료</h3>
<p>이때 동료와의 컨텍스트 중요한데, 내가 말하는 걸 상대가 100% 이해하는 건 힘들다. 
신입의 입장에서 선임에게** &quot;자주 질문하는게 낫다. 단, 어느정도 찾아보고 질문해야한다&quot;
**
→ 혼자 딥하게 찾아보면 나중에는 방향성이 잘못되어 수정하는 데 시간이 더 걸린다.
→ 여러번 맞은 방향성으로 가고 있는지 질문하는게 Best!</p>
<h3 id="4-성장에-대한-호기심">4. 성장에 대한 호기심</h3>
<p>시니어가 되어도 계속 공부해야한다. 
<del>회사 퇴근하고 나서도 주말에도 추가공부하신다고 한다...</del></p>
<h3 id="5-주어진-일만-하는-개발자보다는-일을-만드는-개발자">5. 주어진 일만 하는 개발자보다는 일을 만드는 개발자</h3>
<p>사소한 거라도 주변 동료에게 도움이 되는것 만들기</p>
<h3 id="6-현업자가-어떤일을-하는지-들여다보기">6. 현업자가 어떤일을 하는지 들여다보기</h3>
<p>기술 블로그, 기술 컨퍼런스 영상 활용하기</p>
<hr>
<h3 id="✅-회고-나의-실천-체크리스트">✅ 회고) 나의 실천 체크리스트</h3>
<p>강의를 듣고 나서 어떤 부분을 보완해야하는지 리스트 형태로 만들었다.
앞으로 하나씩 지워나가면서 해당 내용들을 나에게 적용해봐야겠다👀 </p>
<ul>
<li><input disabled="" type="checkbox"> CS 공부한 내용 코드로 적용해서 이해하기</li>
<li><input disabled="" type="checkbox"> CS 공부한 내용은 블로그에 작성하고, ChatGPT와 대화하면서 정리하기</li>
<li><input disabled="" type="checkbox"> 깃허브(GitHub) 깔끔하게 정리하기</li>
<li><input disabled="" type="checkbox"> 기존 프로젝트 &#39;Gymmate&#39; 유지보수 하기</li>
<li><input disabled="" type="checkbox"> 이력서/포트폴리오 꾸준히 수정하고 업데이트하기</li>
</ul>
<hr>
<h3 id="다시보기-링크🔗">다시보기 링크🔗</h3>
<p><a href="https://youtube.com/@programmerschannel?si=TDmfQd52f-HAmE_s">https://youtube.com/@programmerschannel?si=TDmfQd52f-HAmE_s</a>  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Swagger  Content-Type 'application/octet-stream' is not supported 오류]]></title>
            <link>https://velog.io/@se_kite/Swagger-Content-Type-applicationoctet-stream-is-not-supported-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@se_kite/Swagger-Content-Type-applicationoctet-stream-is-not-supported-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Fri, 11 Apr 2025 17:39:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://i.postimg.cc/Gt4zQBgZ/image.png" alt=""></p>
<h3 id="문제-상황">문제 상황</h3>
<p>프론트에서 Swagger UI로 API를 실행할 때
@RequestPart로 JSON을 받는 경우, Swagger는 이를 file로 인식해서 Content-Type: application/octet-stream으로 전송하게 되고,
이로 인해 Content-Type &#39;application/octet-stream&#39; is not supported 오류가 발생한다.</p>
<pre><code class="language-java">2025-04-12T02:10:40.517+09:00  WARN 20456 --- [gymmate] 
[nio-8080-exec-5] .m.m.a.ExceptionHandlerExceptionResolver : 
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: 
Content-Type &#39;application/octet-stream&#39; is not supported]</code></pre>
<p>이를 해결하기 위해서는 @RequestPart를 String으로 받고, 컨트롤러 내부에서 직접 JSON을 파싱하는 방식으로 변경해야 한다.</p>
<p>그러면 @Valid 으로 request 에 대해 검증을 진행할 수가 없다.
@RequestPart로 JSON을 받는 경우, Swagger에서 등록, 수정, 삭제에 대해서 API 실행을 할 수 없고 조회하는 것만 가능한데 괜찮은 걸까?</p>
<h3 id="chatgpt-답변">chatGPT 답변</h3>
<p>실제 운영 환경에서는 대부분 API 테스트를 Postman, Insomnia, 실제 프론트엔드 연동으로 진행해.
Swagger는 문서 + 스펙 안내 도구 역할이 더 크고,
Swagger에서 실행이 안 된다고 해서 API 자체가 문제 있는 건 아니다.</p>
<blockquote>
<p>결론, 지금 구조 유지 + Swagger 설명 강화 + Postman 테스트 제공</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Swagger CORS 오류]]></title>
            <link>https://velog.io/@se_kite/Swagger-CORS-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@se_kite/Swagger-CORS-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Thu, 10 Apr 2025 10:21:42 GMT</pubDate>
            <description><![CDATA[<p><img src="https://i.postimg.cc/t4XJP4px/image.png" alt=""></p>
<h3 id="문제-발생">문제 발생</h3>
<p>FE팀에서 개발 중 Swagger UI에서 API 테스트를 위해 <code>Execute</code> 버튼을 눌렀는데 CORS 오류가 발생했다. 처음엔 Spring Security 설정에 문제가 있는 줄 알고 CORS origin을 추가했지만, 문제가 해결되지 않았다. 원인을 찾아보다가 Swagger 자체 설정도 고려해야 한다는 것을 알게 되었고, 결국 SwaggerConfig와 SecurityConfig를 함께 설정하면서 문제를 해결할 수 있었다.</p>
<p><a href="https://aandi.tistory.com/52">해결을 위해 참고한 블로그</a></p>
<hr>
<h3 id="cors란">CORS란?</h3>
<p>CORS(Cross-Origin Resource Sharing)는 <strong>브라우저의 보안 정책 중 하나</strong>로, 다른 origin으로 요청을 보낼 때 이를 제어한다. 여기서 origin은 <code>프로토콜 + 도메인 + 포트</code> 조합을 의미한다.</p>
<p>예를 들어, 클라이언트가 <code>http://localhost:3000</code>에서 실행 중인데 API 서버는 <code>http://localhost:8080</code>이라면, 서로 다른 origin으로 판단되어 브라우저가 보안 상 요청을 차단한다. 이때 서버가 명시적으로 해당 origin을 허용해야만 정상적인 요청이 가능하다.</p>
<hr>
<h3 id="언제-cors-설정이-필요-없을까">언제 CORS 설정이 필요 없을까?</h3>
<p>Swagger UI가 백엔드 서버와 동일한 origin에서 실행될 경우, 예를 들어 <code>http://localhost:8080/swagger-ui/index.html</code>처럼 Swagger 문서가 백엔드 서버에서 함께 서빙되고 있을 때는 CORS 오류가 발생하지 않는다. 이 경우에는 브라우저 입장에서 동일한 origin으로의 요청이기 때문에 <code>setAllowedOrigins()</code> 설정 없이도 API 요청이 가능하다.</p>
<hr>
<h3 id="언제-cors-설정이-필요할까">언제 CORS 설정이 필요할까?</h3>
<p>반대로, 프론트엔드 개발 중 Swagger UI를 <code>http://localhost:3000</code> 같은 다른 origin에서 띄우는 경우, Swagger가 백엔드 서버(<code>localhost:8080</code>)로 요청을 보내면서 브라우저가 CORS 정책 위반으로 차단한다.</p>
<p>이런 경우에는 Spring Security에서 아래와 같이 CORS 설정을 해줘야 한다.</p>
<pre><code class="language-java">configuration.setAllowedOrigins(List.of(&quot;http://localhost:3000&quot;));</code></pre>
<hr>
<h3 id="swaggerconfig도-함께-설정해야-하는-이유">SwaggerConfig도 함께 설정해야 하는 이유</h3>
<p>CORS 설정만으로 문제가 해결되지 않는 경우가 있다. Swagger UI는 OpenAPI 문서에 명시된 서버 주소를 기준으로 API 요청을 보내는데, 별도로 서버 주소를 설정하지 않으면 Swagger가 문서를 호스팅하고 있는 origin이 아닌 다른 경로로 요청을 보낼 수도 있다.</p>
<p>이럴 때는 SwaggerConfig에 아래와 같이 서버 주소를 명시적으로 설정해줘야 한다.</p>
<pre><code class="language-java">@Bean
public OpenAPI openAPI() {
    return new OpenAPI()
        .addServersItem(new Server().url(&quot;/&quot;));
}</code></pre>
<p>이 설정은 Swagger가 현재 문서를 띄우고 있는 origin을 기준으로 상대경로로 API 요청을 보내도록 한다.</p>
<hr>
<h3 id="정리">정리</h3>
<p>Swagger를 사용할 때 CORS 문제 없이 API 요청이 잘 되기 위해서는 아래 두 가지 설정이 모두 필요할 수 있다.</p>
<table>
<thead>
<tr>
<th>설정</th>
<th>목적</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td><code>SwaggerConfig.addServersItem(...)</code></td>
<td>Swagger UI가 올바른 주소로 요청하도록 설정</td>
<td>잘못된 경로로의 요청 방지</td>
</tr>
<tr>
<td><code>SecurityConfig.setAllowedOrigins(...)</code></td>
<td>해당 origin에서 오는 요청을 허용</td>
<td>브라우저의 CORS 오류 방지</td>
</tr>
</tbody></table>
<p>배포 환경에서는 실제 도메인(<code>https://example.com</code>)도 CORS 허용 목록에 포함해줘야 한다. 프론트엔드 개발 환경(localhost:3000)에서도 Swagger를 통해 API 테스트를 원활하게 하려면 위의 설정을 모두 확인해봐야한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인텔리제이 8080 포트충돌]]></title>
            <link>https://velog.io/@se_kite/%EC%9D%B8%ED%85%94%EB%A6%AC%EC%A0%9C%EC%9D%B4-8080-%ED%8F%AC%ED%8A%B8%EC%B6%A9%EB%8F%8C</link>
            <guid>https://velog.io/@se_kite/%EC%9D%B8%ED%85%94%EB%A6%AC%EC%A0%9C%EC%9D%B4-8080-%ED%8F%AC%ED%8A%B8%EC%B6%A9%EB%8F%8C</guid>
            <pubDate>Thu, 10 Apr 2025 01:27:48 GMT</pubDate>
            <description><![CDATA[<p>인텔리제이에서 어제까지 잘 실행하다가 오늘 갑자기 실행하니까 아래와 같은 port 충돌 오류가 났다</p>
<pre><code class="language-bash">Web server failed to start. Port 8080 was already in use. 
Action: Identify and stop the process that&#39;s listening on port 8080 
or configure this application to listen on another port.</code></pre>
<p>찾아보니까 대부분 아래와 같은 방식의 해결방법으로 진행하더라</p>
<ol>
<li>cmd를 관리자 모드로 실행</li>
<li><code>netstat -ano | findstr 8080</code> 명렁어 입력</li>
<li>taskkill /f 1234 /PID 명령어로 강제 종료</li>
<li>인텔리제이 재실행</li>
</ol>
<p>근데 찾아본 방법으로 진행했는데도 현재 실행중인 8080포트에 대해서 아무런 값이 나오지 않았다. </p>
<p><img src="https://i.postimg.cc/mknXsP2z/image.png" alt=""></p>
<p>그래서 다른 방법으로 포트 충돌을 해결했다.</p>
<ol>
<li>cmd를 관리자 모드로 실행</li>
<li><code>net stop winnat</code> 과<code>net start winnat</code> 명령어 입력</li>
<li>인텔리제이 재실행</li>
</ol>
<h3 id="net-stop-winnat-과-net-start-winnat-명령어">net stop winnat 과 net start winnat 명령어</h3>
<p><strong>winnat 이란?</strong>
WinNAT(Windows Network Address Translation)은 Windows에서 NAT(Network Address Translation) 기능을 제공하는 서비스이다.
특히, Windows에서 Docker, WSL2, Hyper-V 등의 가상화 네트워크를 사용할 때 이 서비스가 백그라운드에서 네트워크 주소를 매핑해준다. </p>
<ul>
<li><p><code>net stop winnat</code> 
→ WinNAT 서비스를 중지해 네트워크 포트 포워딩 설정 등이 해제되는 걸 의미한다</p>
</li>
<li><p><code>net start winnat</code>
→ WinNAT 서비스를 다시 시작</p>
</li>
</ul>
<p><strong>포트 충돌 문제가 해결된 이유?</strong></p>
<ul>
<li><p>8080 포트를 점유하고 있던 게 일반 프로세스가 아니라, winnat을 통해 설정된 포트 포워딩이었을 가능성이 있다.
→ 이 경우 netstat로도 안 보이거나 LISTENING 상태가 아니어서 PID가 안 나올 수 있다.</p>
</li>
<li><p>Docker를 사용한다면 내부적으로 8080 포트를 컨테이너에 포워딩할 수 있고, 이건 WinNAT을 통해 처리된다. </p>
</li>
</ul>
<h3 id="만약-windows에서-8080-충돌-발생했는데-netstat--ano-해도-안-보인다면">만약 Windows에서 8080 충돌 발생했는데 netstat -ano 해도 안 보인다면?</h3>
<ul>
<li>WSL2 접속 &gt;&gt; <code>wsl</code></li>
<li>내부에서 포트 확인  &gt;&gt; `sudo netstat -tuln</li>
<li>현재 8080 포트를 LISTEN 프로세스가 누군지 파악 &gt;&gt; <code>lsof -i :8080 | grep LISTEN | awk &#39;{print $1, $2}&#39;</code></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[docker mysql 3306 포트 충돌]]></title>
            <link>https://velog.io/@se_kite/docker-mysql-3306-%ED%8F%AC%ED%8A%B8-%EC%B6%A9%EB%8F%8C</link>
            <guid>https://velog.io/@se_kite/docker-mysql-3306-%ED%8F%AC%ED%8A%B8-%EC%B6%A9%EB%8F%8C</guid>
            <pubDate>Thu, 10 Apr 2025 00:47:02 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-java">(HTTP code 500) server error - Ports are not available: exposing port TCP 0.0.0.0:3306 -&gt; 0.0.0.0:0: listen tcp4 0.0.0.0:3306: bind: An attempt was made to access a socket in a way forbidden by its access permissions.</code></pre>
<ol>
<li><p>3306 포트를 사용 중인 프로세스 ID(PID) 찾기
명령 프롬프트(CMD)를 관리자 권한으로 실행한 후, 아래 명령어를 입력</p>
<pre><code class="language-bash">netstat -aon | findstr :3306</code></pre>
</li>
<li><p>해당 PID의 프로세스 강제 종료
PID를 확인한 후 아래 명령어로 프로세스를 종료</p>
<pre><code class="language-bash">taskkill /PID 1234 /F</code></pre>
</li>
</ol>
<h4 id="결과">결과</h4>
<p><img src="https://i.postimg.cc/YqSxkWzn/image.png" alt=""></p>
<h4 id="문제상황">문제상황</h4>
<p>pc가 새롭게 시작될때마다 docker에서 실행중인 mysql에서 포트 충돌이 발생하는데 이유가 있나?</p>
<ol>
<li>컴퓨터를 종료한 뒤에 아래 명령어를 했을때 실행이 되는지, 문제가 발생하는지 실험<pre><code>cmd 에서
docker exec -it [mysql_container_name] mysql -u root -p</code></pre></li>
</ol>
<p>위 명령어로 실행했을때 접속이 제대로 되었다. 그러면 뭐가 문제였을까?</p>
<p>이전에 수업 진행하면서 MySQL Workbench에서 실습을 진행했었는데 여기에서 3306으로 포트가 연결된게 있었나보다..</p>
<p>docker에서도 3306으로 mysql을 실행하다보니 포트 충돌이 발생된 듯 하다. 윈도우에서 mysql 관련된 부분을 제거하니 연결이 잘 됐다. 
<img src="https://i.postimg.cc/xd85wMCY/image.png" alt="">
특히 mysqlServer 부분 제거하니 DBeaver에 연결이 잘 됐다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GitHub Actions - GHCR에서 Docker 이미지 Pull 중 발생한 문제 해결]]></title>
            <link>https://velog.io/@se_kite/github-action-GHCR%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-pull-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EB%AC%B8%EC%A0%9C-%EB%B0%9C%EC%83%9D</link>
            <guid>https://velog.io/@se_kite/github-action-GHCR%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-pull-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EB%AC%B8%EC%A0%9C-%EB%B0%9C%EC%83%9D</guid>
            <pubDate>Wed, 19 Mar 2025 10:24:10 GMT</pubDate>
            <description><![CDATA[<p><img src="https://i.postimg.cc/bJvzxs8S/image.png" alt=""></p>
<h3 id="🔍-문제를-찾아간-과정">🔍 문제를 찾아간 과정</h3>
<ul>
<li>GitHub Actions에서 실행한 deploy.yml의 Job 수행 과정에서 문제가 발생한 지점을 확인했다.</li>
<li>태그(Tag) 생성, 릴리즈(Release) 작업, 도커 이미지 빌드 및 푸시까지는 정상적으로 완료되었다.
그러나 새로운 이미지를 다운로드하고 컨테이너를 실행하는 과정에서 오류가 발생했다.
→  <strong>docker run 명령이 정상적으로 실행되지 않아 컨테이너가 시작되지 않음.</strong></li>
</ul>
<p><img src="https://i.postimg.cc/ZYBpnf9r/image.png" alt=""></p>
<h3 id="🚨-문제-원인-잘못된-레지스트리-경로-사용">🚨 문제 원인: 잘못된 레지스트리 경로 사용</h3>
<ul>
<li><p>GHCR과 Docker Hub는 별개의 레지스트리</p>
</li>
<li><p><em>ghcr.io는 GitHub Container Registry(GHCR)*</em>이고, docker.io는 Docker Hub임.
즉, GHCR에서 이미지를 가져오려면 GitHub 사용자명 또는 GitHub 조직명을 사용해야 함.</p>
</li>
<li><p>GitHub 사용자명과 Docker Hub ID는 다를 수 있음</p>
</li>
</ul>
<h3 id="문제가-발생한-코드">문제가 발생한 코드</h3>
<p><code>docker pull ghcr.io/[dockerhub 아이디]/${{ env.DOCKER_IMAGE_NAME }}:latest</code></p>
<p><span style="color: red;"> <strong>즉, dockerhub 아이디가 아닌 git 사용자명으로 작성해야한다</strong></p>
<h3 id="해결된-코드-올바른-경로로-수정">해결된 코드 (올바른 경로로 수정)</h3>
<p><code>docker pull ghcr.io/[github 아이디]/${{ env.DOCKER_IMAGE_NAME }}:latest</code></p>
<hr>
<h3 id="오류가-발생했는데도-job이-실패로-표시되지-않는-이유">오류가 발생했는데도 Job이 실패로 표시되지 않는 이유</h3>
<ul>
<li><p>GitHub Actions에서 실행된 명령어가 실패했어도 GitHub Actions가 이를 감지하지 못하는 경우가 있음.</p>
<ul>
<li>특히 <strong>외부 실행 환경(AWS SSM, SSH, Docker 내부 실행 등)</strong>에서 오류가 발생할 경우, GitHub Actions 자체는 실행 성공(Success)으로 인식할 가능성이 있음.</li>
</ul>
</li>
</ul>
<h3 id="🔍-오류를-빠르게-찾는-방법">🔍 오류를 빠르게 찾는 방법</h3>
<ol>
<li><p>GitHub Actions 로그를 확인
: Actions 페이지에서 실행된 로그를 확인하여 오류 메시지를 찾기.</p>
</br>  </li>
<li><p>set -e로 명령이 실패하면 즉시 종료하도록 설정</p>
<pre><code class="language-sh">set -e
docker pull ghcr.io/[dockerhub아이디]/${{ env.DOCKER_IMAGE_NAME }}:latest</code></pre>
</li>
<li><p>echo $?로 Exit Code 확인
: Exit Code가 0이 아니면 오류가 발생한 것을 의미</p>
<pre><code class="language-sh">docker pull ghcr.io/[dockerhub아이디]/${{ env.DOCKER_IMAGE_NAME }}:latest
echo &quot;Docker Pull Exit Code: $?&quot;</code></pre>
</li>
<li><p>set -x로 실행되는 명령어 로그 출력</p>
<pre><code class="language-sh">set -x
docker pull ghcr.io/[dockerhub아이디]/${{ env.DOCKER_IMAGE_NAME }}:latest</code></pre>
</li>
</ol>
<h3 id="정리-및-요약">정리 및 요약</h3>
<table>
<thead>
<tr>
<th>방법</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>set -e</code></strong></td>
<td>명령어가 실패하면 즉시 종료하여, 오류 발생 시 Job이 계속 진행되지 않도록 방지</td>
</tr>
<tr>
<td><strong><code>echo $?</code></strong></td>
<td>각 명령어의 종료 상태(Exit Code)를 출력하여, 오류 발생 여부를 직접 확인 가능</td>
</tr>
<tr>
<td><strong><code>set -x</code></strong></td>
<td>실행되는 모든 명령어를 로그에 출력하여, 실행 흐름을 디버깅하고 환경 변수 값을 확인 가능</td>
</tr>
</tbody></table>
<blockquote>
<p><code>set -e</code>, <code>echo $?</code>, <code>set -x</code> 모두 GitHub Actions에서 실행되는 <strong>deploy.yml 내의 run 단계에서 추가하면, 해당 Job에서 발생하는 오류를 효과적으로 파악</strong>할 수 있다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[HA Proxy]]></title>
            <link>https://velog.io/@se_kite/HA-Proxy</link>
            <guid>https://velog.io/@se_kite/HA-Proxy</guid>
            <pubDate>Wed, 19 Mar 2025 03:23:43 GMT</pubDate>
            <description><![CDATA[<h3 id="✅-haproxy의-역할과-동작-방식">✅ HAProxy의 역할과 동작 방식</h3>
<p>HAProxy는 <strong>로드 밸런서(reverse proxy)</strong>로서, 새 버전을 배포하는 동안에도 기존 서비스가 중단되지 않도록 트래픽을 관리한다.</p>
<h4 id="1-트래픽을-현재-사용-가능한healthy-서버로-보낸다">1. 트래픽을 현재 사용 가능한(healthy) 서버로 보낸다.</h4>
<ul>
<li>예를 들어, <code>v1</code>이 실행 중일 때 <code>v2</code>를 배포하는 경우, HAProxy는 <code>v2</code>가 준비될 때까지 계속 <code>v1</code>으로 트래픽을 보낸다.</li>
</ul>
<h4 id="2-health-check상태-검사를-수행하여-정상적인-서버만-사용한다">2. Health Check(상태 검사)를 수행하여 정상적인 서버만 사용한다.</h4>
<ul>
<li>새 버전(<code>v2</code>)이 정상적으로 실행되는지 지속적으로 확인한다.</li>
<li>만약 <code>v2</code>가 아직 준비되지 않았다면, 기존 버전(<code>v1</code>)으로 계속 트래픽을 보낸다.</li>
</ul>
<h4 id="3-새-버전v2이-완전히-배포되면-트래픽을-전환한다">3. 새 버전(<code>v2</code>)이 완전히 배포되면 트래픽을 전환한다.</h4>
<ul>
<li><code>v2</code>가 준비되면, 기존 요청을 끊지 않고 점진적으로 <code>v1 → v2</code>로 트래픽을 전환한다.</li>
<li><code>v1</code>이 더 이상 사용되지 않으면 제거할 수 있다.</li>
</ul>
<h3 id="실습1">실습1</h3>
<p><code>localhost:8080 → ha_prox_1(:8090) → nginx_1</code>
: 즉, 8090으로 요청이 들어오면 ha proxy가 8080인 nginx 페이지로 전환한다.</p>
<p><img src="https://i.postimg.cc/0j2nc030/image.png" alt=""></p>
<pre><code class="language-dockerfile">#ha_proxy 설정파일 생성

echo -e &quot;
frontend http_front 
    bind *:80
    default_backend http_back  #80포트 들어온 모든 요청을 http_back으로 보낸다

backend http_back
    server app_server 172.17.0.1:8080
&quot; &gt; volumes/usr/local/etc/haproxy/haproxy.cfg</code></pre>
<ul>
<li><code>frontend</code>와 <code>backend</code>는 HAProxy의 예약어(설정 블록 키워드)</li>
<li>http_front와 http_back은 사용자가 설정한 변수명임</li>
</ul>
<hr>
<h3 id="실습2">실습2</h3>
<p><code>8080(nginx_1)과 8081(nginx_2)로 라운드로빈</code></p>
<pre><code class="language-dockerfile">echo -e &quot;
frontend http_front
    bind *:80
    default_backend http_back

backend http_back
    balance roundrobin  #라운드로빈
    server app_server_1 172.17.0.1:8080 check
    server app_server_2 172.17.0.1:8081 check
&quot; &gt; ${PWD}/dockerProjects/ha_proxy_1/volumes/usr/local/etc/haproxy/haproxy.cfg
</code></pre>
<ul>
<li>localhost:8090으로 요청할 때, 8080(nginx_1)과 8081(nginx_2)이 번갈아가면서 응답</li>
</ul>
<p><img src="https://i.postimg.cc/DZYBZYB4/image.png" alt=""></p>
<hr>
<h3 id="실습3">실습3</h3>
<p><code>app1-127-0-0-1.nip.io =&gt; ha proxy =&gt; nginx_1:80</code>
<code>- 출력 : Web Server 1</code>
<code>app2-127-0-0-1.nip.io =&gt; ha proxy =&gt; nginx_2:80</code>
<code>- 출력 : Web Server 2</code></p>
<pre><code class="language-dockerfile">frontend http_front
    bind *:80
    acl host_app1 hdr(host) -i app1-127-0-0-1.nip.io
    acl host_app2 hdr(host) -i app2-127-0-0-1.nip.io

    use_backend http_back_1 if host_app1
    use_backend http_back_2 if host_app2

backend http_back_1
    server app_server_1 nginx_1:80 check

backend http_back_2
    server app_server_2 nginx_2:80 check
&quot; &gt; ${PWD}/dockerProjects/ha_proxy_1/volumes/usr/local/etc/haproxy/haproxy.cfg</code></pre>
<ul>
<li>app1-127-0-0-1.nip.io으로 들어오면, host_app1에 넣는다.</li>
</ul>
<p><img src="https://i.postimg.cc/j540WTB3/image.png" alt=""></p>
<hr>
<h3 id="실습4">실습4</h3>
<p><code>app1-127-0-0-1.nip.io &gt; ha proxy =&gt; nginx_1_1:80 &gt; Web Server 1-1 출력</code>
<code>app1-127-0-0-1.nip.io &gt; ha proxy =&gt; nginx_1_2:80 &gt; Web Server 1-2 출력</code>
<code>app2-127-0-0-1.nip.io &gt; ha proxy =&gt; nginx_2_1:80 &gt; Web Server 2-1 출력</code>
<code>app2-127-0-0-1.nip.io &gt; ha proxy =&gt; nginx_2_2:80 &gt; Web Server 2-2 출력</code> </p>
<pre><code class="language-dockerfile">## 설정파일 생성
echo -e &quot;
frontend http_front
    bind *:80
    acl host_app1 hdr(host) -i app1-127-0-0-1.nip.io
    acl host_app2 hdr(host) -i app2-127-0-0-1.nip.io

    use_backend http_back_1 if host_app1
    use_backend http_back_2 if host_app2

backend http_back_1
    balance roundrobin
    server app_server_1_1 nginx_1_1:80 check
    server app_server_1_2 nginx_1_2:80 check

backend http_back_2
    balance roundrobin
    server app_server_2_1 nginx_2_1:80 check
    server app_server_2_2 nginx_2_2:80 check
&quot; &gt; ${PWD}/dockerProjects/ha_proxy_1/volumes/usr/local/etc/haproxy/haproxy.cfg
</code></pre>
<ul>
<li>server app_server_1_1 nginx_1_1:8081 check에서 app_server_1_1은 변수(X), 그냥 식별자(O)</li>
<li>server app_server_1_1을 server site_server_1_1처럼 바꿔도 된다!
✔ 그 이유는 app_server_1_1은 변수(X), 단순한 HAProxy 내부 식별자(O)이기 때문이다.</li>
</ul>
<p><img src="https://i.postimg.cc/3Jn8SXSf/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker 멀티스테이지 빌드]]></title>
            <link>https://velog.io/@se_kite/Docker-%EB%A9%80%ED%8B%B0%EC%8A%A4%ED%85%8C%EC%9D%B4%EC%A7%80-%EB%B9%8C%EB%93%9C</link>
            <guid>https://velog.io/@se_kite/Docker-%EB%A9%80%ED%8B%B0%EC%8A%A4%ED%85%8C%EC%9D%B4%EC%A7%80-%EB%B9%8C%EB%93%9C</guid>
            <pubDate>Tue, 18 Mar 2025 02:24:09 GMT</pubDate>
            <description><![CDATA[<h3 id="멀티스테이지-빌드multi-stage-build">멀티스테이지 빌드(Multi-stage Build)</h3>
<p>: 하나의 Dockerfile 내에서 여러 개의 <code>FROM</code>을 사용하여 <strong>빌드 환경과 실행 환경을 분리하는 기법</strong>  </p>
<hr>
<h2 id="📌-왜-멀티스테이지-빌드가-필요할까">📌 왜 멀티스테이지 빌드가 필요할까?</h2>
<ol>
<li><p><strong>빌드 과정과 실행 과정 분리</strong><br>→ 빌드용 이미지에는 많은 개발 도구가 필요하지만, 실행용 이미지는 최대한 가벼운 것이 좋음.  </p>
</li>
<li><p><strong>컨테이너 크기 최적화</strong><br>→ 빌드가 끝난 후 불필요한 파일(예: 소스코드, 빌드 도구)을 제외하여 <strong>작고 빠른 컨테이너</strong>를 만들 수 있음.  </p>
</li>
</ol>
<hr>
<h2 id="docker-멀티스테이지-빌드-최적화">Docker 멀티스테이지 빌드 최적화</h2>
<h3 id="🔹-빌드-최적화란">🔹 빌드 최적화란?</h3>
<p>Docker는 <strong>레이어(layer)</strong> 기반으로 이미지를 빌드하며, 각 <code>RUN</code>, <code>COPY</code>, <code>ADD</code> 등의 명령어가 <strong>새로운 레이어를 생성</strong>한다.<br>이미 캐시된 레이어가 있으면 다시 실행하지 않고 그대로 재사용하여 <strong>빌드 속도를 단축</strong>할 수 있다.  </p>
<h3 id="🔹-최적화-원칙">🔹 최적화 원칙</h3>
<ol>
<li><strong>변하지 않는 부분</strong>(예: 패키지 설치, 의존성 다운로드)은 <strong>최대한 위쪽</strong>에 배치하여 <strong>캐시를 유지</strong>하도록 한다.  </li>
<li><strong>자주 변경되는 코드</strong>(예: 애플리케이션 소스 코드, 설정 파일)는 <strong>아래쪽</strong>에 배치하여 변경이 발생해도 최소한의 레이어만 다시 빌드되도록 한다.  </li>
</ol>
<hr>
<h2 id="⚠-비효율적인-dockerfile-예제-잘못된-순서">⚠ 비효율적인 Dockerfile 예제 (잘못된 순서)</h2>
<pre><code class="language-dockerfile">FROM openjdk:23 AS builder

# 1️⃣ 애플리케이션 소스 코드 복사 (자주 변경됨)
COPY . /app
WORKDIR /app

# 2️⃣ 패키지 설치 및 빌드 (자주 변경되지 않음)
RUN ./gradlew build</code></pre>
<p><strong>❌ 문제점</strong><br>소스 코드(<code>COPY . /app</code>)가 <strong>위쪽에 배치되어 있기 때문에</strong>, 코드가 변경될 때마다 <strong>모든 이후 레이어가 다시 빌드</strong>된다.<br>즉, <code>gradlew build</code>가 <strong>매번 실행되어 비효율적이다.</strong>  </p>
<hr>
<h2 id="✅-최적화된-dockerfile-예제-캐시-최적화">✅ 최적화된 Dockerfile 예제 (캐시 최적화)</h2>
<pre><code class="language-dockerfile">FROM openjdk:23 AS builder

# 1️⃣ 의존성 설치 (자주 변경되지 않음)
WORKDIR /app
COPY build.gradle settings.gradle ./
RUN ./gradlew dependencies

# 2️⃣ 애플리케이션 소스 코드 복사 (자주 변경됨)
COPY src ./src
RUN ./gradlew build</code></pre>
<h3 id="🔴-최적화된-이유">🔴 최적화된 이유</h3>
<p>✔ <code>gradlew dependencies</code> 실행 결과가 <strong>캐시되므로</strong> 소스 코드만 변경되었을 때 전체를 다시 빌드하지 않는다.<br>✔ <code>COPY src ./src</code>가 <strong>가장 아래쪽</strong>에 위치하여, 코드 변경 시 <strong>최소한의 레이어만 다시 빌드된다.</strong>  </p>
<blockquote>
<p><strong>변하지 않는 코드(의존성 설치, 환경 설정)는 위쪽, 자주 변경되는 코드(소스 코드, 설정 파일)는 아래쪽에 배치</strong>해야 한다.<br>이 원칙을 따르면 <strong>빌드 속도를 크게 단축</strong>할 수 있다. 🚀  </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS 가상 메모리 4GB 추가 방법]]></title>
            <link>https://velog.io/@se_kite/AWS-%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-4GB-%EC%B6%94%EA%B0%80-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@se_kite/AWS-%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-4GB-%EC%B6%94%EA%B0%80-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 17 Mar 2025 17:10:40 GMT</pubDate>
            <description><![CDATA[<p>AWS EC2를 프리티어로 사용하면 RAM 메모리가 1GB밖에 되지 않는다
이러면 해당 메모리에 docker나 nginx, spring Boot 빌드만 해도 터진다</p>
<p>이럴땐 <strong>EBS(SSD)에스왑파일을 생성해 가상 메모리를 만들어 저장 공간을 늘릴 수 있다</strong></p>
<h4 id="🤷♀️-ebsssd--elastic-block-store">🤷‍♀️ EBS(SSD,  Elastic Block Store)?</h4>
<p>*AWS에서 제공하는 네트워크 기반의 스토리지
*AWS에서 제공하는 OS 입장에서 보면 하나의 블록 디바이스(디스크)처럼 보이기 때문에,
우리가 직접 Swap 용도로 사용할 &quot;임의의 4GB 파일&quot;을 만들 수 있다. </p>
<p>OS는 실제  RAM (1GB) 사용 → RAM이 부족하면 Swap(SSD/EBS) 사용 
: <code>free -h</code> 명령어를 실행하면 RAM + Swap을 합쳐서 총 가용 메모리처럼 표시됨.
<img src="https://i.postimg.cc/kg58mbGD/image.png" alt=""></p>
<h4 id="1-ebsssd-위에-4gb128mb-x-32-크기의-빈-파일을-생성">1. EBS(SSD) 위에 4GB(128MB X 32) 크기의 빈 파일을 생성</h4>
<p><code>sudo dd if=/dev/zero of=/swapfile bs=128M count=32</code></p>
<h4 id="2-스왑-파일의-읽기-및-쓰기-권한을-업데이트">2. 스왑 파일의 읽기 및 쓰기 권한을 업데이트</h4>
<p><code>sudo chmod 600 /swapfile</code></p>
<h4 id="3-linux-스왑-영역을-설정">3. Linux 스왑 영역을 설정</h4>
<p><code>sudo mkswap /swapfile</code></p>
<h4 id="4-스왑-공간에-스왑-파일을-추가">4. 스왑 공간에 스왑 파일을 추가</h4>
<p><code>sudo swapon /swapfile</code></p>
<h4 id="5-프로시저가-성공적인지-확인">5. 프로시저가 성공적인지 확인</h4>
<p><code>sudo swapon -s</code></p>
<h4 id="6-etcfstab-파일을-편집하여-부팅-시-스왑-파일을-시작">6. /etc/fstab 파일을 편집하여 부팅 시 스왑 파일을 시작</h4>
<p><code>echo &quot;/swapfile swap swap defaults 0 0&quot; &gt;&gt; /etc/fstab</code></p>
<p>→ 테라폼으로 작성해두면 일일히 EC2에 해당 내용 작성해서 적용시키지 않아도 된다!</p>
<pre><code class="language-terraform">locals {
  ec2_user_data_base = &lt;&lt;-END_OF_FILE
#!/bin/bash
yum install docker -y
systemctl enable docker
systemctl start docker

yum install git -y

sudo dd if=/dev/zero of=/swapfile bs=128M count=32
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo sh -c &#39;echo &quot;/swapfile swap swap defaults 0 0&quot; &gt;&gt; /etc/fstab&#39;

END_OF_FILE
}</code></pre>
<h4 id="📌-terraform에서--end_of_file-사용법">📌 Terraform에서 &lt;&lt;-END_OF_FILE 사용법</h4>
<pre><code>
cat &lt;&lt;-EOF
    Hello
        World!
EOF
</code></pre><ul>
<li><code>&lt;&lt;-</code> : 들여쓰기 자동 제거</li>
<li>EOF 이후의 모든 내용이 EOF(End Of File) 마커를 만날 때까지 하나의 문자열로 처리됨</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS (2) 소스 코드 푸시 시 Docker 빌드 및 컨테이너 배포 자동화]]></title>
            <link>https://velog.io/@se_kite/AWS-2</link>
            <guid>https://velog.io/@se_kite/AWS-2</guid>
            <pubDate>Mon, 17 Mar 2025 05:43:59 GMT</pubDate>
            <description><![CDATA[<h3 id="aws-인증-정보를-찾는-우선순위">AWS 인증 정보를 찾는 우선순위</h3>
<p>aws cli가 다운받아져 있고 현재 상황에서 aws configure해서 로그인이 되어 있다면
aws configure list하면 목록이 나온다. </p>
<p>그렇기 때문에 springBoot에서 S3 실행하면 따로 api키를 입력하지 않아도 실행이 된다.
이때, AWS 인증 정보를 찾는 우선순위는 아래와 같다. </p>
<ol>
<li>환경변수</li>
<li>aws configure 명령어로 설정된 list </li>
<li>application.yml</li>
<li>EC2 IAM ROLE</li>
</ol>
<h3 id="도커-빌드-과정에서-테스트-실패를-회피하는-방법-2가지">도커 빌드 과정에서 테스트 실패를 회피하는 방법 2가지</h3>
<p>도커 빌드 과정에서 테스트가 <strong>실패 원인)  해당 환경에서는 aws 인증정보가 없어서</strong></p>
<ol>
<li><p>dockerfile &gt;  <code>RUN gradle build --no-daemon -x test</code>
:  -x test 옵션을 추가하면 Gradle 빌드 시 테스트를 실행하지 않음.</p>
</li>
<li><p>application-test.yml에서 S3 자동 설정 제외
: src/main/resources/application-test.yml에 아래 내용 추가해 테스트 환경에서 S3 서버 찾는 시도를 하지 않도록 막음</p>
</li>
</ol>
<pre><code class="language-yml">spring:
   autoconfigure:
     exclude: io.awspring.cloud.autoconfigure.s3.S3AutoConfiguration</code></pre>
<p>이때 Test class에 <code>@ActiveProfiles(&quot;test&quot;)</code> 추가
<img src="https://i.postimg.cc/L6pwNHXx/image.png" alt=""></p>
<p>🙋‍♀️ <code>@ActiveProfiles(&quot;test&quot;)</code>는 Spring Boot가 application-test.yml을 우선 적용하도록 설정하는 역할을 한다.
일반적으로 테스트 코드에서 특정 프로파일을 강제 적용할 때 사용됨.</p>
<p>만약, <strong>application-testing.yml 파일을 참조하려면 @ActiveProfiles(&quot;testing&quot;)으로 설정해야 한다</strong></p>
<h3 id="docker-이미지-빌드-푸시-방법">docker 이미지 빌드, 푸시 방법</h3>
<blockquote>
<p>Dockerizing 하는 과정</p>
</blockquote>
<p>1) 애플리케이션 코드 준비(git pull)
2) Dockerfile 작성
3) docker build로 Docker 이미지 생성
<code>docker build -t seyoen3/app:8 .</code> : <span style="color:red;"> <code>.</code>빼먹으면 안된다!!</p>
<p>4) Docker push로 Docker hub에 업로드
<code>docker push seyoen3/app:8</code></p>
<p>→ 이후 docker pull로 해당 이미지 다운받아 docker에서 컨테이너로 실행가능
<code>docker pull seyoen3/app:8</code>
<code>docker run -d -p 8080:8080 --name app seyoen3/app:8</code> </p>
<h3 id="docker-이미지-빌드-푸시-span-stylecolorred-자동화">docker 이미지 빌드, 푸시 <span style="color:red;"> 자동화</h3>
<ul>
<li>.github/workflows/deploy.yml에 job 작성  </li>
<li>GitHub Actions를 통해 Docker 이미지를 자동으로 빌드</li>
<li>GitHub Packages(Container Registry)에 저장</li>
<li>해당 패키지 setting에 들어가서 <strong>Private(비공개)</strong>로 설정해야함</li>
</ul>
<p>🙋‍♀️Docker 이미지 내부에 환경 변수, API Key, <strong>민감한 설정 파일이 포함될 가능성이 있어</strong> 보안 상 해당 패키지를 <strong>Private(비공개)</strong>로 설정하는게 좋다</p>
<ul>
<li>비공개 패키지를 EC2에서 다운로드하려면? </li>
</ul>
<ol>
<li>access Token을 생성</li>
<li>로그인 진행<pre><code>docker login ghcr.io -u [github아이디]  //이후, password에 accessToken 입력
</code></pre></li>
</ol>
<pre><code>3. docker pull  ghcr.io/your-org/your-private-image:latest
4. EC2에서 `docker run -d -p 8080:8080 --name app [이미지]` 컨테이너 실행

🙋‍♀️ ghcr.io는 **GitHub Container Registry(GHCR)**의 도메인이며, GitHub Packages에서 제공하는 Docker 이미지 저장소이다. 비공개(Private) 패키지는 EC2에서 다운로드하려면 반드시 **docker login ghcr.io 과정을 거쳐야 한다.**(공개(Public) 패키지는 로그인 없이 다운로드 가능)

---

### 서버 작업 &lt;span style=&quot;color:red;&quot;&gt;  자동화

+ 자동화 대상 : 기존 새 이미지 다운 → 기존 컨테이너 종료 → 새 이미지 실행 → 기존 도커이미지 삭제
+ GITHUB ACTION 에서 도커 이미지 푸시가 완료되면 운영서버에게 교체하라는 명령어를 전송하여 자동배포 완성

1. 리포지터리 세팅에서 AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 시크릿 변수 생성

2. .github/workflows/deploy.yml에 step추가
```dockerfile
   deploy:
    runs-on: ubuntu-latest
    needs: [ buildImageAndPush ]
    steps:
      - name: AWS SSM Send-Command
        uses: peterkimzz/aws-ssm-send-command@master
        id: ssm
        with:
          aws-region: ${{ secrets.AWS_REGION }}
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          instance-ids: &quot;i-015d44766db7d2a9f&quot;
          working-directory: /
          comment: Deploy
          command: |
            docker pull ghcr.io/${{ env.OWNER_LC }}/app20250314:latest
            docker stop app1 2&gt;/dev/null
            docker rm app1 2&gt;/dev/null
            docker run -d --name app1 -p 8080:8080 ghcr.io/${{ env.OWNER_LC }}/app20250314:latest
            docker rmi $(docker images -f &quot;dangling=true&quot; -q)</code></pre><h3 id="명령어">명령어</h3>
<p><code>nslookup</code>: 특정 DNS(Domain Name System) 조회를 수행하는 명령어
<code>curl</code> :  Command-line URL tool의 약자로, 터미널에서 HTTP 요청을 보내고 응답을 확인할 수 있는 명령어</p>
<p>ex. curl <a href="https://example.com">https://example.com</a> → 브라우저에서 페이지를 여는 것과 동일한 효과)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS]]></title>
            <link>https://velog.io/@se_kite/AWS</link>
            <guid>https://velog.io/@se_kite/AWS</guid>
            <pubDate>Thu, 13 Mar 2025 15:34:24 GMT</pubDate>
            <description><![CDATA[<h3 id="1-aws와-테라폼">1. AWS와 테라폼</h3>
<ul>
<li>AWS : AWS(Amazon Web Services)는 아마존에서 제공하는 클라우드 컴퓨팅 서비스</li>
<li>테라폼 :  HashiCorp에서 개발한 인프라스트럭처를 코드로 관리하는 오픈소스 도구
→ Terraform의 선언적 구성(Declarative Configuration) 문법을 사용하여 클라우드 리소스를 생성, 관리, 업데이트할 수 있다</li>
</ul>
<pre><code class="language-java">// AWS 프로바이더 선언
provider &quot;aws&quot; {
  region = var.region
}

✔️ provider &quot;aws&quot; → AWS를 사용하겠다는 선언
✔️ region = var.region → AWS 리전(예: us-east-1, ap-northeast-2)을 변수로 설정
✔️ 이 코드는 Terraform이 AWS 인프라를 관리하는 데 필요한 기본 설정을 정의하는 부분
</code></pre>
<h3 id="✅-ec2-100개-생성-방식-비교-aws-콘솔-vs-terraform">✅ EC2 100개 생성 방식 비교: AWS 콘솔 vs Terraform**</h3>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th><strong>AWS 콘솔 (수동 방식)</strong></th>
<th><strong>Terraform (자동화 방식)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>설정 방식</strong></td>
<td>웹 UI에서 수동으로 EC2를 하나씩 생성</td>
<td>코드로 선언한 후 한 번에 생성</td>
</tr>
<tr>
<td><strong>자동화 수준</strong></td>
<td>❌ 없음 (완전 수동)</td>
<td>✅ 코드 실행 한 번으로 자동 생성</td>
</tr>
<tr>
<td><strong>EC2 100개 생성 시간</strong></td>
<td>⏳ 오래 걸림 (각 인스턴스마다 클릭 필요)</td>
<td>🚀 빠름 (한 번의 <code>terraform apply</code>로 생성)</td>
</tr>
<tr>
<td><strong>오류 가능성</strong></td>
<td>❌ 사람의 실수 가능 (설정 누락, 옵션 잘못 선택)</td>
<td>✅ 일정한 설정 유지 (인프라 일관성 보장)</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>❌ 불편 (100개 수정 시 하나씩 변경 필요)</td>
<td>✅ 편리 (코드 수정 후 <code>terraform apply</code> 실행)</td>
</tr>
<tr>
<td><strong>대량 배포 가능 여부</strong></td>
<td>❌ 어려움 (수작업으로 많은 인스턴스 생성 비효율적)</td>
<td>✅ 쉬움 (변수 설정으로 여러 개 자동 생성)</td>
</tr>
<tr>
<td><strong>재현 가능성</strong></td>
<td>❌ 동일한 환경을 다시 만들기 어려움</td>
<td>✅ 코드 기반으로 동일한 인프라 재현 가능</td>
</tr>
<tr>
<td><strong>삭제 및 정리</strong></td>
<td>❌ 하나씩 삭제해야 함</td>
<td>✅ <code>terraform destroy</code> 한 번으로 모든 EC2 삭제 가능</td>
</tr>
<tr>
<td><strong>버전 관리</strong></td>
<td>❌ 설정 변경 내역 추적 어려움</td>
<td>✅ 코드 기반으로 Git 등 버전 관리 가능</td>
</tr>
</tbody></table>
<h3 id="결론">결론</h3>
<p>✔ <strong>AWS 콘솔</strong>: 클릭 몇 번으로 간단하게 EC2를 생성할 수 있지만, <strong>100개를 만들려면 매우 비효율적이고 실수 가능성이 큼</strong><br>✔ <strong>Terraform</strong>: 선언형 코드로 EC2를 자동화하여 <strong>빠르고 일관되게 배포할 수 있으며 유지보수도 훨씬 편리함</strong>  </p>
<p><strong>즉, 대량의 EC2 인스턴스를 생성하려면 Terraform을 사용하는 것이 훨씬 효율적이고 안정적이다!</strong></p>
<hr>
<h3 id="2-루트-계정과-aws-iam">2. 루트 계정과 AWS IAM</h3>
<h4 id="2-1-관계">2-1. 관계</h4>
<ul>
<li>루트 계정: 최상위 관리자 계정. 해킹 시 심각한 보안 위험 발생 가능</li>
<li>IAM 계정 : 보안 강화를 위해 루트 계정의 직접 사용을 피하고, 필요한 권한만 부여하여 관리할 수 있도록 하기 위해 생성한다</li>
</ul>
<p>ex. admin(사용자)에게  <code>AdministratorAccess 정책</code> 부여</p>
<p><img src="https://i.postimg.cc/rpH4rhs6/image.png" alt=""></p>
<blockquote>
<p>이때, 역할에 부여하는 정책은 하나가 아니라 여러 권한을 모아놓은 묶음이다!!
<strong>즉, 정책(policy) = 권한 묶음</strong></p>
</blockquote>
<h4 id="2-2-로그인-방법">2-2. 로그인 방법</h4>
<ul>
<li>루트 계정 로그인 : 루트계정 + 비밀번호</li>
<li>IAM 계정 로그인 : 루트계정 ID or 루트 계정 별칭 IAM계정이름 + 비밀번호</li>
</ul>
<p>루트계정 ID는 어려우니까 루트 계정 별칭을 만들어주고 사용하는게 좋다</p>
<p><img src="https://i.postimg.cc/Kc02rdFg/image.png" alt=""></p>
<h4 id="2-3-aws-iam">2-3. AWS IAM</h4>
<ul>
<li><p>AWS IAM에서 권한을 부여할 수 있는 대상은 크게 <code>사용자(User), 그룹(Group), 역할(Role)</code> 3가지이다.</p>
</li>
<li><p>역할(Role)에 권한을 부여한다는 의미 = 특정 AWS 리소스나 외부 사용자가 임시로 사용할 수 있는 권한을 부여한다는 의미</p>
</li>
<li><p><span style="color: red;"> <strong>IAM은 글로벌 서비스로 특정 리전에 귀속되지 않는다.</strong></p>
<p>🤷‍♀️<strong>why?</strong> AWS 계정(Account) 단위로 역할(Role)을 생성하고 부여하는 방식이기 때문에
만약 IAM이 특정 리전에 귀속된다면, 각 리전에 대해 동일한 IAM 역할을 중복으로 설정해야 하는 불편함이 생긴다.</p>
</li>
</ul>
<hr>
<h3 id="3-여러-용어-설명">3. 여러 용어 설명</h3>
<ul>
<li><p><code>리전</code> : <strong>하나 이상의 데이터센터가 모여</strong> 하나의 가용영역을 형성하는데, 하나의 리전에는 2개 이상의 가용영역(AZ)으로 구성되어 있다. 가용영역은 물리적으로 떨어져있다.</p>
</li>
<li><p><code>VPC((Virtual Private Cloud))</code> : AWS 클라우드 내 <strong>가상 네트워크</strong></p>
<p>✅ <strong>CIDR 블록(서브넷) 비교: <code>/16</code> vs <code>/24</code></strong>  </p>
</li>
</ul>
<table>
<thead>
<tr>
<th>CIDR 블록</th>
<th>네트워크에서 관리하는 IP 개수</th>
<th>서브넷 마스크</th>
<th>사용 가능한 IP 대역</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>10.0.0.0/16</strong></td>
<td><code>65,536개</code> (<code>256 * 256</code>)</td>
<td><code>255.255.0.0</code> (<code>/16</code>)</td>
<td><code>10.0.0.0 ~ 10.0.255.255</code></td>
<td>뒤의 <code>16비트(8*2)</code>를 사용 가능 </br> → 뒤의 2자리</td>
</tr>
<tr>
<td><strong>10.0.0.0/24</strong></td>
<td><code>256개</code> (<code>256</code>)</td>
<td><code>255.255.255.0</code> (<code>/24</code>)</td>
<td><code>10.0.0.0 ~ 10.0.0.255</code></td>
<td>뒤의 <code>8비트(8*1)</code>만 사용 가능 </br> → 뒤의 1자리</td>
</tr>
</tbody></table>
</br>

<h4 id="🔹-비트-표현">🔹 <strong>비트 표현</strong></h4>
<h4 id="1-1000016">1) <strong>10.0.0.0/16</strong></h4>
<p>11111111 11111111 00000000 00000000
(앞의 16비트는 고정, 뒤의 16비트(8<em>2) 사용 가능) → IP 대역: 10.0.*</em>0.0** ~ 10.0.<strong>255.255</strong></p>
<h4 id="2-1000024">2) <strong>10.0.0.0/24</strong></h4>
<p>11111111 11111111 11111111 00000000  → IP 대역: 10.0.0.<strong>0</strong> ~ 10.0.0.<strong>255</strong>
(앞의 24비트는 고정, 뒤의 8비트(8*1) 사용 가능)</p>
<ul>
<li>서브넷 : <strong>VPC에 속해있는 하위 네트워크</strong></li>
<li>라우팅 테이블: 네트워크 <strong>패킷이 어디로 가야 하는지 결정</strong>하는 지도 같은 역할</li>
<li>인터넷 게이트웨이: 퍼블릭 서브넷에서 인터넷과 통신할 때 사용, 이때 게이트웨이 자체는 아무 역할도 하지 않음. <strong>라우팅 테이블에 연결되어야 인터넷 연결이 가능</strong>함</li>
<li>보안그룹 :  EC2 인스턴스에 대한 인바운드 및 아웃바운드 트래픽을 제어하는 <strong>가상 방화벽</strong></li>
</ul>
<hr>
<h3 id="4-테라폼-프로젝트">4. 테라폼 프로젝트</h3>
<ul>
<li>terraform.tfstate,  terraform.tfstate.backup 파일에는 민감한 정보(AWS RDS 비밀번호 등)들이 포함되어 있을 수 있어 <strong>리포지토리를 <code>private</code>로 관리</strong>해야한다.</li>
</ul>
<h4 id="✅-퍼블릭-서브넷을-만들-때-필요한-구성">✅ 퍼블릭 서브넷을 만들 때 필요한 구성</h4>
<ol>
<li>VPC 생성</li>
<li>서브넷 생성 (퍼블릭 서브넷)</li>
<li>인터넷 게이트웨이 생성 및 연결</li>
<li>라우팅 테이블 생성 및 IGW로 경로 추가</li>
<li>퍼블릭 서브넷에 라우팅 테이블 연결</li>
<li>EC2 인스턴스에 퍼블릭 IP 할당</li>
</ol>
<pre><code>             ┌──────────────────────────────────────────────┐
             │              (dev-vpc-1)                 │
             │ CIDR: 10.0.0.0/16                        │
             │                                          │
             │  ┌───────────────────────────────────────┐   │
             │  │   Internet Gateway (dev-igw-1)        │
             │  └────────────────────────────────────────┘  │
             │         │                                 │
             │         ▼                                 │
             │  ┌────────────────────────────────────────┐  │
             │  │   Route Table (dev-rt-1)               │
             │  │   0.0.0.0/0 → IGW                      │
             │  └────────────────────────────────────────┘  │
             │         │            │            │       │
             │         ▼            ▼            ▼       │
             │   ┌──────────┐  ┌──────────┐  ┌──────────┐  │
             │   │ Subnet-1 │  │ Subnet-2 │  │ Subnet-3 │
             │   │ 10.0.1.0 │  │ 10.0.2.0 │  │ 10.0.3.0 │  
             │   └────▲─────┘  └────▲─────┘  └────▲─────┘  │
             │        │            │            │       │
             │   ┌────┴─────┐  ┌────┴─────┐  ┌────┴─────┐  │
             │   │  EC2-1   │  │  EC2-2   │  │  EC2-3   │
             │   │ (Public) │  │ (Public) │  │ (Public) │
             │   └──────────┘  └──────────┘  └──────────┘  │
             └──────────────────────────────────────────────┘</code></pre><h4 id="테라폼-명령어">테라폼 명령어</h4>
<pre><code>terraform init  : 라이브러리 다운로드
terraform plan : 현재 소스 코드가 실행가능한지 검사
terraform apply : 리소스 생성
terraform destory : 리로스 삭제</code></pre><p>→ <span style="color: red;"> ** &quot;<code>terraform apply</code>와 <code>terraform destroy</code> 명령어를 사용하면 AWS 콘솔에서 직접 클릭하며 리소스를 생성하거나 삭제할 필요가 없다.&quot;**</p>
<hr>
<h3 id="5-iam---iam-인스턴스-프로파일---ec2-인스턴스-연결">5. IAM - IAM 인스턴스 프로파일 - EC2 인스턴스 연결</h3>
<table>
<thead>
<tr>
<th>AWS 개념</th>
<th>비유적인 표현</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>IAM Role (IAM 역할)</strong></td>
<td><strong>비행기 운항 매뉴얼</strong></td>
<td>EC2가 수행할 수 있는 권한과 정책을 정의함. 하지만 EC2가 직접 적용할 수 없음.</td>
</tr>
<tr>
<td><strong>IAM Instance Profile (IAM 인스턴스 프로파일)</strong></td>
<td><strong>관제탑</strong></td>
<td>IAM 역할과 EC2를 연결하는 중간 다리 역할. EC2가 IAM 역할을 사용할 수 있도록 전달함.</td>
</tr>
<tr>
<td><strong>EC2 Instance (EC2 서버)</strong></td>
<td><strong>비행기</strong></td>
<td>IAM 역할을 직접 이해할 수 없고, 인스턴스 프로파일을 통해 매뉴얼을 적용받아 AWS 서비스를 사용함.</td>
</tr>
</tbody></table>
  </br>

<ul>
<li>IAM 역할은 EC2에 직접 연결될 수 없고, IAM 인스턴스 프로파일을 통해 연결됨.</li>
<li>IAM 인스턴스 프로파일은 하나의 IAM 역할만 포함 가능 → <code>1:1</code> 관계
→ 하나의 인스턴스에 여러 개의 IAM 역할을 부여하면 역할 간의 정책 충돌이 발생할 가능성이 높음. </li>
</ul>
<p>ex.  IAM 역할 A: S3 읽기만 허용 / IAM 역할 B: S3 전체 접근 허용
EC2 인스턴스가 두 역할을 모두 가지면, 어떤 정책이 적용될지 모호해짐.</p>
<ul>
<li>IAM 인스턴스 프로파일은 여러 EC2 인스턴스에 적용 가능 → <code>1:N</code> 관계</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[3차 프로젝트 발표회]]></title>
            <link>https://velog.io/@se_kite/3%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%9C%ED%91%9C%ED%9A%8C</link>
            <guid>https://velog.io/@se_kite/3%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%9C%ED%91%9C%ED%9A%8C</guid>
            <pubDate>Wed, 12 Mar 2025 06:55:33 GMT</pubDate>
            <description><![CDATA[<h3 id="전체직인-프로젝트-후기">전체직인 프로젝트 후기</h3>
<ul>
<li><p>리팩토링 후 코틀린 마이그레이션 진행하는 방향이 좋다.</p>
</li>
<li><p>성공한 경우에는 <code>resultCode</code>를 <strong>200으로 단일 처리</strong>하는 것이 좋다. 
(실제로 현업에서도 201, 204 등 따로 처리하는 것이 거의 의미가 없다.)</p>
</li>
<li><p>서버의 개수가 늘어나면 Stomp와 SSE에 변화가 필요하다. 중간에 처리하는 외부 브로커를 추가해야 한다.
→ 외부 브로커를 통해 메시지를 중앙에서 관리하면, 여러 서버가 동시에 클라이언트와 통신할 수 있으며, 데이터의 일관성을 유지할 수 있다.</p>
</li>
<li><p>채팅의 경우, 처음부터 Stomp로 시작하기보다는 WebSocket으로 먼저 실습해보는 것이 좋다. </p>
</li>
</ul>
</br>

<h3 id="다른-팀이-도입한-기술-구현-및-효과">다른 팀이 도입한 기술 구현 및 효과</h3>
<table>
<thead>
<tr>
<th>주제</th>
<th>구현 방식</th>
<th>도입한 기술</th>
<th>효과</th>
</tr>
</thead>
<tbody><tr>
<td>팔로우 - 팔로잉 기능</td>
<td>Redis를 사용하여 팔로우/팔로잉 관계를 캐싱하고, Redis 장애 시 DB에서 데이터를 복구하는 스케줄러 구현</td>
<td>Redis, 스케줄러</td>
<td>데이터 일관성 유지, 장애 발생 시 빠른 복구 가능</td>
</tr>
<tr>
<td>도서 검색</td>
<td>n-gram 인덱스를 사용하여 제목과 설명을 인덱싱하고, 두 글자 키워드로 검색 속도 향상</td>
<td>n-gram 인덱스</td>
<td>검색 속도 및 정확도 향상</td>
</tr>
<tr>
<td>채팅/알람 기능</td>
<td>SSE를 구현하여 실시간 알림을 제공하고, Postman으로 테스트하여 기능 확인</td>
<td>SSE, Postman</td>
<td>실시간 데이터 전송, 사용자 경험 개선</td>
</tr>
<tr>
<td>첨부파일 처리</td>
<td>에디터에서 임시 이미지를 업로드하고, 최종 업로드 시 해당 이미지를 파일로 교체하는 방식 구현</td>
<td>이미지 처리 기술</td>
<td>사용자 편의성 증대, 이미지 관리 효율성 향상</td>
</tr>
<tr>
<td>페이지네이션 처리</td>
<td>최근 게시글 위주로 3개월 이내의 게시글만 표시하거나 오래된 게시글을 삭제하여 페이지네이션 최적화</td>
<td>데이터베이스 관리</td>
<td>페이지네이션 개수 감소, 사용자 경험 개선</td>
</tr>
<tr>
<td>휴대폰 인증</td>
<td>사용자가 번호를 입력하면 CoolSMS를 통해 인증번호를 발송하는 기능 구현</td>
<td>CoolSMS API</td>
<td>인증 과정 간소화, 사용자 신뢰도 향상</td>
</tr>
</tbody></table>
 </br>


<hr>
<h3 id="우리팀-pofo-프로젝트-피드백">우리팀, POFO 프로젝트 피드백</h3>
<ul>
<li><p>사용자가 작성한 글을 삭제한 경우, DB에 남겨놓는게 더 좋다.
→ 정책에 따라 DB에 남겨놓지 않을수도 있는데 탈퇴 시 완전 삭제를 진행시키는 방향이 더 좋을 것 같다. </p>
</li>
<li><p>소셜 로그인 계정 통합 및 관리자 대시보드 기능이 잘 구현되어 있으며, 실제 서비스에서도 기본적으로 해당 기능들이 구현된 경우가 많다.
<del>→ 다만, 이메일 인증의 경우 개인정보 보호 방침 때문에 강사님은 이메일과 같은 개인정보를 최대한 받지 않는 방식을 선호하지만 해당 프로젝트는 토이 프로젝트이므로, 해당 내용은 참고하기</del></p>
</li>
</ul>
</br>


<h3 id="추후-프로젝트를-어떻게-고도화하면-좋을까">추후 프로젝트를 어떻게 고도화하면 좋을까?</h3>
<p>아래와 같이 요청하신 내용을 표에 추가하였습니다.</p>
<table>
<thead>
<tr>
<th>고도화 방법</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>성능 개선 수치화 및 마이그레이션 감소 수치화</td>
<td>성능 개선 및 마이그레이션으로 인한 감소 사항을 수치화하면 신뢰도 향상. ✅PPT에 수치가 많을수록 좋다</td>
</tr>
<tr>
<td>AOP를 활용한 로그 저장</td>
<td>AOP를 활용하여 Error 로그 및 Info 로그 저장. logback 설정으로 날짜별, 크기 초과 시 압축 및 일정 기간 후 삭제.</td>
</tr>
<tr>
<td>깃허브 액션 CI 활용</td>
<td>develop 브랜치에 올라오는 PR에 대해 병합 전 자동 빌드 및 테스트 진행. (빌드 시 gradle.test 진행하므로 별도의 test진행할 필요X)</td>
</tr>
<tr>
<td>문의사항 게시판에서 채팅으로 변경</td>
<td>문의사항을 게시판에서 채팅으로 변경. 이미지 첨부, 이미지 저장, 드래그 앤 드랍 기능 추가.</td>
</tr>
<tr>
<td>Docker Compose 활용</td>
<td>운영 및 개발 서버의 시간 감축을 위해 Docker Compose 활용.</td>
</tr>
<tr>
<td>Swagger API 도입</td>
<td>API 문서화를 위해 Swagger API 도입.</td>
</tr>
<tr>
<td>Redis 캐시 활용</td>
<td>API로 내용을 가져올 때 Redis 캐시 사용하여 성능 개선 비교.</td>
</tr>
<tr>
<td>AI 도입</td>
<td><a href="https://4ourfuture.tistory.com/55">chatGPT 가격 정보</a> 고려해야 함. 무료 사용을 원한다면 구글 AI 사용 추천, <a href="https://blog.naver.com/ryurime88/223629579585">SKT 통신사를 이용한다면 Perplexity 사용 추천</a></td>
</tr>
</tbody></table>
<p>cf) 코틀린으로 마이그레이션 시 라인 수 변화 :  스프링 부트로 혜택을 보고 있기 때문에 코틀린으로 마이그레이션 시 30<del>40% 감소는 어렵고, `5%</del>10%가 이상적임` 
→ 만약, 스프링부트로 게임 개발을 한다면 코틀린으로 마이그레이션 시 코드라인 수가 많이 감소한다는 걸 체감할 수 있을 것이다. 
→ <strong>줄어든 라인수에 집착하기 보다는 코드가 코틀린스러운지를 파악하는게 더 좋을듯!</strong></p>
<ul>
<li><a href="https://github.com/prgrms-be-devcourse/NBE3-4-3-Team12">Gemmni AI활용한 PR리뷰 써보기</a> : Gmmini API key env 파일에 넣기</li>
<li><a href="https://github.com/prgrms-be-devcourse/NBE3-4-3-team01/blob/main/backend/src/main/resources/logback-spring.xml">로그저장</a> </li>
</ul>
</br>

<h3 id="추가-공부해야하는-개념">추가 공부해야하는 개념</h3>
<ul>
<li>동시성 이슈 : Redisson Lock</li>
<li>webSocket : RabbitMQ</li>
<li>JPQL → QeuryDSL 변환:  kept, Qclass </li>
<li>페이징 방식 성능 최적화 : 데이터가 많을 수록 offset이 아닌 no-offset 활용해 부하를 줄일 수 있다.</li>
<li>현재 Hibernate lazyloading 문제 발생 중.. JPQL 오류와 관련이 있을 수 있음..</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git 추적, PR필터링, Issue생성, merge commit]]></title>
            <link>https://velog.io/@se_kite/Git-%EC%B6%94%EC%A0%81</link>
            <guid>https://velog.io/@se_kite/Git-%EC%B6%94%EC%A0%81</guid>
            <pubDate>Tue, 11 Mar 2025 17:35:07 GMT</pubDate>
            <description><![CDATA[<h3 id="1-develop-브랜치에서-특정-파일을-수정한-사람-찾는-방법">1. <code>develop</code> 브랜치에서 특정 파일을 수정한 사람 찾는 방법</h3>
<ul>
<li>github에서 확인하기</li>
</ul>
<ol>
<li>github &gt; repository &gt; branch &gt; 해당 파일을 선택한다.  </li>
<li><code>history</code>에서 <code>commit</code> 내역을 확인하여 누가 수정했는지 파악한다.  </li>
</ol>
<p><img src="https://i.postimg.cc/2SDSXxQh/image.png" alt=""></p>
<ul>
<li>CLI로 확인하기</li>
</ul>
<ol>
<li><code>git blame 파일명</code> 명령어를 사용하면 해당 파일의 각 줄을 마지막으로 수정한 사람을 알 수 있다.  </li>
<li><code>git log --follow -- 파일명</code> 명령어를 실행하면 파일의 변경 이력을 추적할 수 있다.  </li>
</ol>
</br>

<h3 id="2-pr을-남긴-사람을-필터링하는-방법">2. PR을 남긴 사람을 필터링하는 방법</h3>
<ol>
<li>PR 목록에서 특정 사용자의 이름을 클릭한다.  </li>
<li>해당 사용자의 PR만 필터링되어 표시된다.  </li>
<li>GitHub에서는 <code>is:pr author:사용자명</code> 형식의 검색어를 활용하면 특정 사용자가 생성한 PR을 쉽게 찾을 수 있다.  </li>
<li><code>git log --author=&quot;사용자명&quot;</code> 명령어를 사용하면 해당 사용자의 <code>commit</code> 내역을 확인할 수 있다.  </li>
</ol>
<p><img src="https://i.postimg.cc/pT1mwtsS/image.png" alt=""></p>
</br>

<h3 id="3issue-생성-및-branch-생성">3.issue 생성 및 branch 생성</h3>
<ol>
<li>작업할 내용을 GitHub <code>Issues</code>에 등록한다.  </li>
<li><code>develop</code> 브랜치를 <code>pull</code>받는다.  </li>
<li><code>local</code>에서 새로운 <code>branch</code>를 만들고 <code>add</code> → <code>commit</code> → <code>push</code>한다.  </li>
<li>PR 내용 작성 시 <code>#[issue번호]</code>로 해당 <code>Issues</code>를 언급한다.  </li>
<li>해당 브랜치를 <code>merge</code>한다.  </li>
<li>모든 <code>Issue</code> 작업이 끝나면 마지막 <code>branch</code>에서 <code>closes #[issue번호]</code>를 붙여 해당 <code>Issue</code>를 <code>close</code>한다.  또는 <code>Issue</code> 탭에서 수동으로 <code>close</code>할 수도 있다.  </br>
### 4. merge commit</li>
</ol>
<ul>
<li><code>merge</code> 시 마지막에 <code>commit</code>하는데, 이 부분의 내용이나 제목은 크게 신경 쓰지 않아도 된다.  </li>
<li>다만, 정상적으로 <code>merge</code>한 것이 아니라 <code>close</code>하는 경우에는 명확한 사유를 남겨야 한다.  <ul>
<li>예: &quot;기능 불필요로 인해 PR close&quot;, &quot;다른 PR과 중복되어 close&quot;  </li>
</ul>
</li>
<li><code>close</code>한 PR이 나중에 다시 필요할 수도 있으므로, 관련 내용을 이슈나 댓글로 남겨두면 좋다.  </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[3차 멘토링]]></title>
            <link>https://velog.io/@se_kite/3%EC%B0%A8-%EB%A9%98%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@se_kite/3%EC%B0%A8-%EB%A9%98%ED%86%A0%EB%A7%81</guid>
            <pubDate>Thu, 06 Mar 2025 15:10:15 GMT</pubDate>
            <description><![CDATA[<h3 id="1-코틀린으로-변환했을-때의-이점-🌟">1. 코틀린으로 변환했을 때의 이점 🌟</h3>
<ul>
<li>자바와 코틀린은 호환되므로 점진적으로 변환 가능</li>
<li>코틀린은 <strong>널포인트익셉션(NPE) 발생이 적음</strong></li>
<li><strong>확장 함수</strong>를 활용하여 기존 클래스에 기능 추가 가능</li>
<li>자바보다 간결한 코드 작성 가능 → but, 요즘은 java 버전이 ↑ 거의 개선됨</li>
<li><strong>코루틴</strong> 등 다양한 기능 제공</li>
</ul>
<p>✅ 코루틴이 뭐지?
: 코틀린에서 제공하는 비동기 프로그래밍을 위한 경량 스레드임.</p>
<h4 id="🚀-예제-2-네트워크-요청-api-1000개-호출---java-vs-kotlin-coroutine">🚀 <strong>예제 2: 네트워크 요청 (API 1000개 호출) - Java vs Kotlin Coroutine</strong></h4>
<p>API를 1000번 호출하는 상황을 가정하고 Java와 Kotlin에서 각각 구현했을 때 성능을 비교해본다.<br>Java에서는 <code>ExecutorService</code>를 사용하여 멀티스레딩을 구현하고, Kotlin에서는 <code>Coroutine</code>을 사용하여 비동기적으로 실행한다.</p>
<hr>
<h3 id="💥-java-executorservice-사용"><strong>💥 Java (ExecutorService 사용)</strong></h3>
<pre><code class="language-java">import java.util.concurrent.*;

public class JavaApiRequest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(100);
        long startTime = System.currentTimeMillis();

        for (int i = 0; i &lt; 1000; i++) {
            executor.execute(() -&gt; {
                try {
                    Thread.sleep(500); // API 요청을 시뮬레이션
                    System.out.println(&quot;API 요청 완료 (Thread: &quot; + Thread.currentThread().getName() + &quot;)&quot;);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(10, TimeUnit.SECONDS);
        long endTime = System.currentTimeMillis();
        System.out.println(&quot;Total execution time: &quot; + (endTime - startTime) + &quot;ms&quot;);
    }
}</code></pre>
<h4 id="🔹-문제점"><strong>🔹 문제점</strong></h4>
<ul>
<li><strong>스레드 풀 크기(100개)</strong> 제한으로 인해 <strong>동시 요청이 제한적</strong>이다.</li>
<li><strong>스레드 컨텍스트 스위칭 비용</strong>이 발생하여 속도가 느려진다.</li>
<li><code>Thread.sleep(500)</code>을 사용하여 <strong>스레드가 불필요하게 블로킹</strong>된다.</li>
</ul>
<hr>
<h3 id="💡-kotlin-coroutine-사용"><strong>💡 Kotlin (Coroutine 사용)</strong></h3>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() = runBlocking {
    val time = measureTimeMillis {
        val jobs = List(1000) {
            launch(Dispatchers.IO) {
                delay(500L) // API 요청을 시뮬레이션
                println(&quot;API 요청 완료 (Coroutine: ${Thread.currentThread().name})&quot;)
            }
        }
        jobs.forEach { it.join() }
    }
    println(&quot;Total execution time: ${time}ms&quot;)
}</code></pre>
<h4 id="🔹-장점"><strong>🔹 장점</strong></h4>
<ul>
<li><strong>Dispatchers.IO</strong>를 사용하여 <strong>비동기적으로 실행</strong>되므로 스레드 제한 없이 많은 요청을 처리할 수 있다.</li>
<li><code>delay(500L)</code>을 사용하여 <strong>스레드를 차단하지 않고</strong> 효율적으로 실행한다.</li>
<li><strong>코루틴 컨텍스트 스위칭 비용이 적어</strong> 성능이 향상된다.</li>
</ul>
<h3 id="🔥-java-vs-kotlin-coroutine-성능-비교"><strong>🔥 Java vs Kotlin Coroutine 성능 비교</strong></h3>
<table>
<thead>
<tr>
<th></th>
<th><strong>Java (Thread 사용)</strong></th>
<th><strong>Kotlin (Coroutine 사용)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>실행 방식</strong></td>
<td><code>ExecutorService</code>를 사용하여 스레드 풀 생성</td>
<td><code>launch</code>를 사용하여 경량 코루틴 실행</td>
</tr>
<tr>
<td><strong>동시 실행 가능 개수</strong></td>
<td>제한적 (스레드 풀 크기에 따라 결정)</td>
<td>무제한 (스레드보다 가벼움)</td>
</tr>
<tr>
<td><strong>컨텍스트 스위칭 비용</strong></td>
<td>높음 (스레드 간 전환 비용 발생)</td>
<td>낮음 (코루틴 컨텍스트 전환 최적화)</td>
</tr>
<tr>
<td><strong>메모리 사용량</strong></td>
<td>높음 (스레드당 메모리 할당 필요)</td>
<td>낮음 (코루틴은 스택 메모리를 적게 사용)</td>
</tr>
<tr>
<td><strong>지연 작업</strong></td>
<td><code>Thread.sleep()</code>으로 인해 블로킹 발생</td>
<td><code>delay()</code>를 사용하여 비동기 실행</td>
</tr>
<tr>
<td><strong>처리 속도</strong></td>
<td>약 <strong>5~10초</strong> 소요 (스레드 스케줄링 영향)</td>
<td>약 <strong>1초</strong> 소요 (코루틴 최적화)</td>
</tr>
</tbody></table>
<hr>
<p>Q. 실무에서 코틀린과 자바 혼용해서 쓰기도 하나?
A. 일부 기업에서는 혼용 가능
멘토님 회사의 경우도 스칼라→ 코틀린으로 전환함( 코틀린이 레퍼런스가 더 많아서 ) </p>
<h3 id="2-코틀린으로-전환하는-방식">2. 코틀린으로 전환하는 방식</h3>
<ul>
<li>서비스 단위로 변경 가능</li>
<li>DTO도 단계적으로 변환 가능</li>
<li>데이터 클래스 활용 추천
Tip) AI를 활용한 PR리뷰를 통해 <code>코틀린스러운</code> 코드인지 질문해보기!
Tip) IDE에서 제미나이(Gemini) 활용 추천 <a href="https://codeassist.google/#available-in-your-favorite-ides-and-platforms">다운 링크</a></li>
</ul>
<h3 id="3-사이드-프로젝트-및-실무-경험">3. 사이드 프로젝트 및 실무 경험</h3>
<ul>
<li>실제 현업에서도 업무량에 따라 사이드 프로젝트 진행 가능
개인 프로젝트가 물경력이 되는 것을 방지하기 위해 노력 필요
성공 사례: 사이드 프로젝트(<a href="https://withcoffee.app/developer">소개팅 앱</a>)로 높은 수익을 올려 퇴사 후 창업한 사례 존재
단, 앱 개발 시 약관 동의, 개인정보 처리 등 민감 정보 처리 경험 필요</li>
</ul>
<h3 id="4-배포-관련-비용-문제">4. 배포 관련 비용 문제</h3>
<ul>
<li>프로젝트 규모에 따라 비용 발생 가능
초기에는 무료 프리티어 활용 가능</li>
<li><em>오라클 클라우드 프리티어(VM 2개 제공) 
→ 사이드 프로젝트로 사용(오라클 VM에 DB 올려서 사용) 추천!!! *</em>
MySQL은 무료가 없지만 MongoDB 무료 버전 존재</li>
</ul>
<h3 id="5-mongodb와-redis-비교-🌟">5. MongoDB와 Redis 비교 🌟</h3>
<ul>
<li>Redis : 휘발성 메모리 기반, 영속성 없음, single 쓰레드 기반이라 사용 시 주의
→ DB가 아니다!!!!!</li>
<li>MongoDB : 디스크 저장, 데이터 처리 및 집계 기능 제공, 트랜잭션 및 ACID 중 몇가지 지킴</li>
</ul>
<h3 id="6-취업-시-자바-코틀린-vs-nodejs-python">6. 취업 시 자바, 코틀린 vs Node.js, Python</h3>
<ul>
<li>한국에서는 <code>자바 &amp; 스프링이 대세</code></li>
<li>Python은 데이터 처리 및 AI 관련 분야에서 사용</li>
<li>Node.js는 풀스택 역할 요구하는 경우 많음 → <del>풀스택 바라는 곳은 가지마...</del></li>
<li>그래도 가고 싶은 특정 기업이 요구하는 기술이 있다면 학습 필요</li>
</ul>
<p>ex. 한국에서는 코볼(COBOL)거의 사용 안하지만, 미국에서는 시니어 개발자 부족으로 높은 연봉으로 모셔가는 사례가 생김</p>
<h3 id="7-사이드-프로젝트-경험">7. 사이드 프로젝트 경험</h3>
<ul>
<li>꾸준한 사이드 프로젝트 진행 추천</li>
<li>Task 관리의 경우, <a href="https://trello.com/">트렐로(Trello)</a> 추천</li>
<li>WebSocket 을 활용한 프로젝트도 해보면 좋다!</li>
</ul>
<p>ex. 멘토님의 경우, 게임 관련 앱을 개발해 6개월간 광고 수익 450만 원 달성 경험O
→ 애드센스 적극 활용하길 추천!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트 상세 조회 시 UI에 데이터가 표시되지 않는 문제 ]]></title>
            <link>https://velog.io/@se_kite/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%81%EC%84%B8-%EC%A1%B0%ED%9A%8C-%EC%8B%9C-UI%EC%97%90-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%ED%91%9C%EC%8B%9C%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@se_kite/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%81%EC%84%B8-%EC%A1%B0%ED%9A%8C-%EC%8B%9C-UI%EC%97%90-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%ED%91%9C%EC%8B%9C%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Wed, 05 Mar 2025 06:05:37 GMT</pubDate>
            <description><![CDATA[<h4 id="1-문제-상황"><strong>1. 문제 상황</strong></h4>
<p>프로젝트 상세 조회 API를 호출한 후, 응답 데이터가 정상적으로 반환되고 <code>setProject(res.data);</code>를 통해 상태를 업데이트했지만, 화면(UI)에는 데이터가 표시되지 않는 문제가 발생하였다.</p>
<h4 id="2-문제-원인-분석"><strong>2. 문제 원인 분석</strong></h4>
<ul>
<li>API 응답을 확인한 결과, <code>res.data</code>의 구조는 다음과 같았다.<pre><code class="language-json">{
  &quot;resultCode&quot;: &quot;200&quot;,
  &quot;message&quot;: &quot;프로젝트 단건 조회가 완료되었습니다.&quot;,
  &quot;data&quot;: {
    &quot;projectId&quot;: 36,
    &quot;name&quot;: &quot;이세연&quot;,
    &quot;startDate&quot;: &quot;2025-03-04&quot;,
    &quot;endDate&quot;: &quot;2025-03-05&quot;,
    &quot;memberCount&quot;: 1
  }
}</code></pre>
</li>
<li>기존 코드에서 <code>setProject(res.data);</code>로 상태를 설정했기 때문에, <code>project</code>의 상태 구조가 다음과 같아졌다.<pre><code class="language-tsx">{
  resultCode: &quot;200&quot;,
  message: &quot;프로젝트 단건 조회가 완료되었습니다.&quot;,
  data: {
    projectId: 36,
    name: &quot;이세연&quot;,
    startDate: &quot;2025-03-04&quot;,
    endDate: &quot;2025-03-05&quot;,
    memberCount: 1
  }
}</code></pre>
</li>
<li>그러나, UI에서는 <code>project.name</code>, <code>project.startDate</code>와 같은 방식으로 데이터를 출력하려고 하였고, 실제로 <code>name</code> 값은 <code>project.data.name</code>에 존재하였다.</li>
<li>즉, <code>project.name</code>을 참조하려 했으나 <code>undefined</code>가 반환되었기 때문에 화면에 아무것도 출력되지 않았다.</li>
</ul>
<h4 id="3-해결-방법"><strong>3. 해결 방법</strong></h4>
<ul>
<li><code>setProject(res.data.data);</code>로 변경하여 <code>data</code> 내부의 객체만 저장하도록 수정하였다.<pre><code class="language-tsx">setProject(res.data.data);</code></pre>
</li>
<li>이렇게 변경한 후, <code>project</code>의 상태 구조는 다음과 같이 변경되었다.<pre><code class="language-tsx">{
  projectId: 36,
  name: &quot;이세연&quot;,
  startDate: &quot;2025-03-04&quot;,
  endDate: &quot;2025-03-05&quot;,
  memberCount: 1
}</code></pre>
</li>
<li>이제 <code>project.name</code>, <code>project.startDate</code> 등 UI에서 참조하는 속성들이 올바르게 매칭되었고, 정상적으로 화면에 표시되었다.</li>
</ul>
<h4 id="4-결론"><strong>4. 결론</strong></h4>
<ul>
<li>API 응답에서 불필요한 <code>resultCode</code>, <code>message</code>까지 포함하여 상태를 저장하면, UI에서 데이터를 참조할 때 예상치 못한 문제가 발생할 수 있다.</li>
<li><code>setProject(res.data.data);</code>와 같이 <strong>API 응답에서 실제 필요한 데이터만 상태에 저장</strong>해야 한다.</li>
<li>이를 통해 <code>project.name</code>, <code>project.startDate</code> 등의 속성을 UI에서 바로 사용할 수 있게 되었고, 정상적으로 데이터가 렌더링되었다. 🚀</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[multipart/form-data와 JSON 동시 처리]]></title>
            <link>https://velog.io/@se_kite/multipartform-data%EC%99%80-JSON-%EB%8F%99%EC%8B%9C-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@se_kite/multipartform-data%EC%99%80-JSON-%EB%8F%99%EC%8B%9C-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Fri, 28 Feb 2025 19:27:52 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-발생">문제 발생</h3>
<p>프로젝트 수정 시, 다음과 같은 세 가지 경우가 존재한다.</p>
<ol>
<li><strong>썸네일 첨부파일만 수정하는 경우</strong> 
→ <code>Content-Type &#39;application/octet-stream&#39; is not supported.</code></li>
<li><strong>썸네일 첨부파일과 프로젝트 내용(JSON)도 수정하는 경우</strong> 
→ <code>Content-Type &#39;application/json&#39; is not supported.</code></li>
<li><strong>프로젝트 내용(JSON)만 수정하는 경우</strong> 
→ <code>Content-Type &#39;application/octet-stream&#39; is not supported.</code></li>
</ol>
<p>각각의 경우에서 <code>415 Unsupported Media Type</code> 오류가 발생했다</p>
<h3 id="문제-원인">문제 원인</h3>
<p>Spring Boot에서 <code>415 Unsupported Media Type</code> 오류가 발생한 이유는 <strong>Postman에서 deleteThumbnail 값을 Text로 보내지 않았을 가능성이 크다.</strong></p>
<h4 id="원인-boolean-타입을-requestpart로-받을-경우-발생하는-문제">원인) Boolean 타입을 @RequestPart로 받을 경우 발생하는 문제</h4>
<pre><code class="language-java">@RequestPart(value = &quot;deleteThumbnail&quot;, required = false) Boolean deleteThumbnail,</code></pre>
<p>위와 같이 Boolean을 <code>@RequestPart</code>로 받을 경우, Spring Boot는 <code>multipart/form-data</code>에서 Boolean 값을 직접 처리하지 못한다.</p>
<ul>
<li>Boolean 값은 <code>true</code> 또는 <code>false</code> 값을 기대하지만, <code>multipart/form-data</code>는 기본적으로 <strong>String 또는 File 형태</strong>로 전달된다.</li>
<li>Postman에서 <code>false</code>를 입력해도 내부적으로 <code>application/octet-stream</code>으로 인식될 수 있다.</li>
<li>Spring Boot는 <code>multipart/form-data</code> 내에서 Boolean 값을 직접 변환하지 못해 오류가 발생한다.</li>
</ul>
<p>💡 <strong>즉, Postman이 Boolean 값을 Text가 아닌 <code>application/octet-stream</code> 타입으로 보내면서 Spring Boot가 이를 지원하지 못한 것이다.</strong></p>
<h3 id="문제-해결-방법-string으로-수신-후-boolean-변환">문제 해결 방법: <code>String</code>으로 수신 후 Boolean 변환</h3>
<p><code>deleteThumbnail</code>을 <strong>String으로 받고</strong> Boolean으로 변환하여 사용하는 방식으로 해결했다. </p>
<pre><code class="language-java">@RequestPart(value = &quot;deleteThumbnail&quot;, required = false) String deleteThumbnailStr, // ✅ String으로 받기</code></pre>
<p>이제 <code>deleteThumbnail</code>을 <code>String</code>으로 받으면서 Postman이 데이터를 반드시 <strong>Text 형식으로 보내도록 강제</strong>할 수 있다.</p>
<ul>
<li>Postman에서 <code>&quot;false&quot;</code>를 Text로 보내면, Spring Boot는 이를 String으로 처리할 수 있다.</li>
<li>이후 Boolean으로 변환하여 사용하면 된다.</li>
</ul>
<pre><code class="language-java">Boolean deleteThumbnail = deleteThumbnailStr != null &amp;&amp; deleteThumbnailStr.equalsIgnoreCase(&quot;true&quot;);</code></pre>
<p>이 방식은 Spring이 <code>multipart/form-data</code>를 더 자연스럽게 처리할 수 있도록 도와준다.</p>
<hr>
<h3 id="추가-학습">추가 학습</h3>
<h4 id="1-multipartform-data와-json을-함께-처리하기-위한-원리">1. multipart/form-data와 JSON을 함께 처리하기 위한 원리</h4>
<ul>
<li><code>multipart/form-data</code>는 여러 개의 데이터를 <strong>각각의 파트로 분리</strong>하여 전송하는 방식이다.</li>
<li>JSON과 파일을 함께 전송하려면 각각의 Content-Type을 적절하게 설정해야 한다.</li>
<li>Spring에서는 <code>@RequestPart</code>를 활용하여 JSON과 파일을 동시에 받을 수 있다.</li>
</ul>
<h4 id="2-requestparam-vs-requestpart">2. @RequestParam vs @RequestPart</h4>
<h4 id="requestparam"><code>@RequestParam</code></h4>
<ul>
<li>쿼리 파라미터, form 데이터, <code>multipart/form-data</code> 등의 요청 파라미터를 처리할 수 있다.</li>
<li>메소드 인수가 <code>String</code>이나 <code>multipart/form-data</code>가 아닐 경우 <strong>등록된 Converter 또는 PropertyEditor</strong>를 통한 형식 변환에 의존한다.</li>
<li>Key/Value <strong>form 필드와 함께 사용</strong>된다.</li>
</ul>
<h4 id="requestpart">@RequestPart</h4>
<ul>
<li><code>multipart/form-data</code>에 특화되어 있으며 <strong>여러 복잡한 값</strong>을 처리할 때 사용할 수 있다.</li>
<li>메소드 파라미터 타입이 <code>String</code> 또는 <code>MultipartFile/Part</code>가 아닐 경우, 요청 부분의 <code>Content-Type</code> 헤더를 고려하는 <strong>HttpMessageConverters</strong>에 의존한다.</li>
<li>JSON, XML 등을 포함하는 <strong>복잡한 Content를 포함하는 경우에 적합</strong>하다.</li>
</ul>
<h4 id="3-spring의-multipart-처리-방식">3. <strong>Spring의 Multipart 처리 방식</strong></h4>
<ul>
<li><code>CommonsMultipartResolver</code> vs <code>StandardServletMultipartResolver</code> 비교 → <code>CommonsMultipartResolver</code>는 Apache Commons FileUpload를 기반으로 동작하며, <code>StandardServletMultipartResolver</code>는 Servlet 3.0의 기본 Multipart 기능을 활용한다.</li>
<li><code>spring.servlet.multipart.enabled</code> 설정 옵션 → true로 설정하면 Spring Boot에서 Multipart 요청을 자동으로 처리할 수 있다.</br>
#### 4. **Postman에서 multipart/form-data 전송 시 고려해야 할 사항**</li>
<li>파일 전송 시 <code>form-data</code> 방식으로 설정하는 방법 → Postman에서 Body 탭에서 <code>form-data</code> 선택 후 Key에 파일명을 지정하고 Type을 File로 설정해야 한다.</li>
<li>Boolean 값 전달 시 <code>Text</code>로 설정해야 하는 이유 → <code>multipart/form-data</code>에서는 Boolean 값이 기본적으로 지원되지 않으므로 String으로 변환 후 처리해야 한다.</br>
#### 5. **Spring에서 JSON을 처리하는 방법**</li>
<li><code>@RequestBody</code> vs <code>@RequestPart</code> 차이점 → <code>@RequestBody</code>는 JSON 전체 요청을 변환하고, <code>@RequestPart</code>는 Multipart 요청의 일부로 JSON을 처리한다.</li>
<li><code>HttpMessageConverter</code>를 활용한 커스텀 변환 적용 방법 → Spring의 <code>MappingJackson2HttpMessageConverter</code>를 사용하면 JSON을 원하는 객체로 변환할 수 있다.</br>
#### 6. **파일 업로드 관련 보안 이슈**</li>
<li>업로드 파일의 Content-Type 검증 → 파일 확장자만 검증하는 것이 아니라 실제 Content-Type을 검사해야 한다.</li>
<li>파일 크기 제한 설정 (<code>spring.servlet.multipart.max-file-size</code>) → 너무 큰 파일이 업로드되지 않도록 적절한 크기 제한을 설정해야 한다.</li>
<li>업로드된 파일의 저장 위치 및 접근 제어 전략 → 업로드된 파일을 서버 내부에 저장하는 경우 접근 권한을 제한해야 한다.</li>
</ul>
<hr>
<h2 id="정리">정리</h2>
<blockquote>
<p>Spring Boot에서 <code>multipart/form-data</code>와 JSON을 함께 처리할 때는 <code>@RequestPart</code>를 사용해야 하며, Boolean 값을 <code>@RequestPart</code>로 받을 경우 <strong>String으로 변환한 후 처리</strong>하는 것이 안전하다.</p>
</blockquote>
<blockquote>
<p>Postman에서 데이터를 전송할 때는 반드시 <code>Text</code> 형식으로 보내야 하며, <code>multipart/form-data</code>를 사용할 경우 각 필드의 타입을 정확하게 지정하는 것이 중요하다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker(6) 보완 필요]]></title>
            <link>https://velog.io/@se_kite/Docker6</link>
            <guid>https://velog.io/@se_kite/Docker6</guid>
            <pubDate>Fri, 28 Feb 2025 05:40:58 GMT</pubDate>
            <description><![CDATA[<p>nginx가 없으면 포트를 다르게 해서 연결해야한다.
nginx는 컨트롤러, 거대한 if,switch문, 배구에서의 토스와 같은 역할을 한다.
<img src="https://i.postimg.cc/HsYDNntK/image.png"></p>
<p>+nginx가 없다면, 포트를 다 다르게 해서 매핑해줘야한다.
a.com:8080 → site1
b.com:8081 → site2
c.com:8082 → site3</p>
<p>+nginx가 있다면, 같은 포트로 요청이 들어와도 nginx가 분배?를 해준다.
a.com:80 → nginx → site1
b.com:80 → nginx → site2
c.com:80 → nginx → site3</p>
<p>브라우저와 nginx가 주고받는 정보는 암호화되어 있다.
(http: 암호화X , htpps : 암호화O)</p>
<p>포트로 구분 / 도메인으로 구분하는건 nginx에게는 쉽지만
사람에게는 도메인으로 구분하는게 편하다   →  nginx를 사용해야한다!!</p>
<h4 id="리버스-프록시">리버스 프록시</h4>
<p>: 가장 먼저 nginx 가 정적파일캐시, SSL관련처리를 한 후 요청을 앱서버에게 토스하는 방식</p>
<p>스프링부트(app-1)를 먼저 띄우고 그 이후에 nginx(my-nginx-1)를 띄워야 한다.
초기 nginx가 시작할 때 <a href="http://app-1:8090">http://app-1:8090</a> 이 없다면 오류가 발생하기 때문이다.
이후 만약, 스프링부트(app-1) 업데이트를 위해 잠시 stop해 놓는다고 nginx는 꺼지지 않는다 → 다만 502 Bad Gateway 오류가 발생할 수 있음.
그렇기 때문에 처음에 nginx 컨테이너를 만들때만 순서를 유의해주면 된다.</p>
<p>=========67강, 68강 실습
<code>docker build -t myname/app-1 .</code> 명령어를 사용하는 이유는 <strong>Dockerfile을 기반으로 커스텀 도커 이미지를 만든 후, 이 이미지를 이용하여 컨테이너를 실행하기 위함</strong>이다</p>
<ol>
<li><p><strong>Dockerfile 작성</strong>  </p>
<ul>
<li>원하는 설정과 애플리케이션을 포함한 커스텀 이미지를 정의</li>
</ul>
</li>
<li><p><strong><code>docker build</code> 명령어 실행</strong>  </p>
<ul>
<li>Dockerfile을 기반으로 이미지를 생성</li>
</ul>
</li>
<li><p><strong><code>docker run</code> 명령어로 컨테이너 실행</strong>  </p>
<ul>
<li>생성한 이미지를 기반으로 컨테이너를 실행</li>
</ul>
</li>
</ol>
<h4 id="문제1">문제1</h4>
<blockquote>
<p>도커 컴포즈로 구성
<a href="http://app1-127-0-0-1.nip.io">http://app1-127-0-0-1.nip.io</a> =&gt; app-1 스프링부트(proxy pass)
<a href="http://app2-127-0-0-1.nip.io">http://app2-127-0-0-1.nip.io</a> =&gt; my-nginx-1:/web/site2/index.html</p>
</blockquote>
<h4 id="문제2">문제2</h4>
<blockquote>
<ul>
<li>도커 컴포즈로 구성
<a href="http://app1-127-0-0-1.nip.io">http://app1-127-0-0-1.nip.io</a> =&gt; app-1 스프링부트(proxy pass)
<a href="http://app1new-127-0-0-1.nip.io">http://app1new-127-0-0-1.nip.io</a> =&gt; app-1 스프링부트(proxy pass)
<a href="http://app2-127-0-0-1.nip.io">http://app2-127-0-0-1.nip.io</a> =&gt; app-2 스프링부트(proxy pass)
<a href="http://app2new-127-0-0-1.nip.io">http://app2new-127-0-0-1.nip.io</a> =&gt; app-2 스프링부트(proxy pass)
<a href="http://app1-127-0-0-1.nip.io">http://app1-127-0-0-1.nip.io</a> 와 <a href="http://app1new-127-0-0-1.nip.io">http://app1new-127-0-0-1.nip.io</a> (은)는 같은 스프링 부트로 연결됩니다.
<a href="http://app2-127-0-0-1.nip.io">http://app2-127-0-0-1.nip.io</a> 와 <a href="http://app2new-127-0-0-1.nip.io">http://app2new-127-0-0-1.nip.io</a> (은)는 같은 스프링 부트로 연결됩니다.
app-2 는 기존 app-1 의 소스코드를 가지고 만들어주세요.</li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker(5)]]></title>
            <link>https://velog.io/@se_kite/Docker5</link>
            <guid>https://velog.io/@se_kite/Docker5</guid>
            <pubDate>Thu, 27 Feb 2025 06:59:06 GMT</pubDate>
            <description><![CDATA[<p>62강.
용량을 확인해보면 알겠지만
나중에 rm으로 삭제한다고 해서 용량이 줄어들지 않는다.</p>
<p>RUN 명령어를 쓴다는것이 layer를 추가하는거고 , 이때 layer는 immuatable하다
그렇기떄문에 싱글이 아닌 멀티 스테이지 빌드를 사용하는 것이 용량 측면에서도 사용하기 좋다!</p>
<h3 id="도메인">도메인</h3>
<p>A레코드(like 메모)</p>
<p>abc.com(도메인) : 특정 IP 이름?</p>
<h4 id="nslookup-명령어">nslookup 명령어</h4>
<p><code>nplookup</code> abc.com(도메인)
: A레코드를 알 수 있다. </p>
<img src="https://i.postimg.cc/Dz0rNRf9/image.png">
어떤 IP주소로 연결될지는 알 수 없다. 매번 연결마다 랜덤하게 진행한다 → `RoundRobine 방식`
: 서버가 부하를 줄이기 위해 분산을 진행하고 있음을 알 수 있다.


]]></description>
        </item>
        <item>
            <title><![CDATA[Docker(4)]]></title>
            <link>https://velog.io/@se_kite/Docker4</link>
            <guid>https://velog.io/@se_kite/Docker4</guid>
            <pubDate>Thu, 27 Feb 2025 05:10:03 GMT</pubDate>
            <description><![CDATA[<h3 id="1-java에서-build와-실행-파일jar의-차이">1. Java에서 Build와 실행 파일(JAR)의 차이</h3>
<h4 id="1️⃣-빌드build란">1️⃣ 빌드(Build)란?</h4>
<ul>
<li><strong>빌드(Build)</strong>는 <code>.java</code> 파일을 <strong>컴파일</strong>하여 <code>.class</code>(바이트코드) 파일로 변환하는 과정이다.</li>
<li><code>javac</code>(Java Compiler)를 사용하여 <code>.java</code> 파일을 <code>.class</code> 파일로 변환한다.</li>
</ul>
<h4 id="컴파일-예시">컴파일 예시</h4>
<pre><code class="language-sh">javac Test.java</code></pre>
<ul>
<li>위 명령을 실행하면 <code>Test.class</code> 파일이 생성된다.</li>
</ul>
<h4 id="2️⃣-실행-파일jar-java-archive">2️⃣ 실행 파일(JAR, Java Archive)</h4>
<ul>
<li><strong>JAR(Java ARchive)</strong> 파일은 여러 개의 <code>.class</code> 파일과 라이브러리를 하나의 압축 파일로 패키징한 것이다.</li>
<li><code>.class</code> 파일만으로 실행하기 불편하기 때문에, <strong>JAR 파일로 패키징하여 실행을 쉽게 만든다</strong>.</li>
</ul>
<h4 id="jar-파일-생성-예시">JAR 파일 생성 예시</h4>
<pre><code class="language-sh">jar cf Test.jar Test.class</code></pre>
<p>또는 특정 디렉터리(<code>build/</code>) 안의 파일들을 JAR로 묶는 경우:</p>
<pre><code class="language-sh">jar cf Test.jar -C build/ .</code></pre>
<ul>
<li>위 명령을 실행하면 <code>Test.jar</code> 파일이 생성된다.</li>
</ul>
<hr>
<h4 id="3️⃣-jar-파일-실행">3️⃣ JAR 파일 실행</h4>
<ul>
<li>JAR 파일을 실행하려면 <code>java -jar</code> 명령어를 사용해야 한다.</li>
</ul>
<h4 id="jar-실행-예시">JAR 실행 예시</h4>
<pre><code class="language-sh">java -jar Test.jar</code></pre>
<h3 id="📝-요약">📝 요약</h3>
<table>
<thead>
<tr>
<th>단계</th>
<th>개념</th>
<th>예시 명령어</th>
</tr>
</thead>
<tbody><tr>
<td><strong>빌드 (컴파일)</strong></td>
<td><code>.java</code> 파일을 <code>.class</code> 파일로 변환하는 과정</td>
<td><code>javac Test.java</code></td>
</tr>
<tr>
<td><strong>패키징 (JAR 생성)</strong></td>
<td><code>.class</code> 파일을 JAR로 묶는 과정</td>
<td><code>jar cf Test.jar Test.class</code></td>
</tr>
<tr>
<td><strong>실행 (JAR 실행)</strong></td>
<td>JAR 파일을 실행하는 과정</td>
<td><code>java -jar Test.jar</code></td>
</tr>
</tbody></table>
<p> <strong>즉, 빌드는 <code>.java</code> 파일을 <code>.class</code> 파일로 변환하는 과정이며, JAR 파일은 실행 가능한 형태로 패키징한 것이다.</strong> </p>
<hr>
<h3 id="2-바인드-마운트bind-mount와-볼륨volume의-차이">2. 바인드 마운트(Bind Mount)와 볼륨(Volume)의 차이</h3>
<p>Docker에서는 <strong>컨테이너와 호스트 간 데이터 공유</strong>를 위해 <strong>바인드 마운트(Bind Mount)</strong>와 <strong>볼륨(Volume)</strong> 두 가지 방식을 제공한다.<br>둘 다 데이터를 컨테이너와 공유할 수 있도록 하지만, 동작 방식과 사용 목적이 다르다.</p>
<h4 id="✅-1-바인드-마운트bind-mount">✅ 1. 바인드 마운트(Bind Mount)</h4>
<h4 id="개념">개념</h4>
<ul>
<li><strong>바인드 마운트는 호스트의 특정 디렉터리를 컨테이너 내부로 연결하는 방식이다.</strong></li>
<li>컨테이너가 호스트의 파일 시스템을 직접 접근하여 읽고 쓸 수 있다.</li>
<li>컨테이너가 종료되어도 <strong>호스트의 원본 파일은 그대로 유지된다.</strong></li>
<li>컨테이너와 호스트 간 <strong>파일 동기화가 즉시 이루어진다.</strong></li>
</ul>
<h4 id="사용-예시">사용 예시</h4>
<pre><code class="language-sh">docker run -d --name bind-container \
  -v /home/user/app:/app \
  nginx</code></pre>
<ul>
<li><strong>호스트의 <code>/home/user/app</code></strong> 디렉터리를 <strong>컨테이너의 <code>/app</code></strong>에 마운트한다.</li>
<li>컨테이너 내부에서 <code>/app</code> 경로를 수정하면 <strong>호스트의 <code>/home/user/app</code>도 함께 변경된다.</strong></li>
</ul>
<h4 id="✅-2-볼륨volume">✅ 2. 볼륨(Volume)</h4>
<h4 id="개념-1">개념</h4>
<ul>
<li><strong>볼륨은 Docker가 자체적으로 관리하는 저장소를 사용한다.</strong></li>
<li><strong>컨테이너와 독립적으로 데이터가 저장되며</strong>, 여러 컨테이너 간에 공유할 수 있다.</li>
<li><strong>Docker가 <code>/var/lib/docker/volumes/</code> 디렉터리에서 데이터를 관리한다.</strong></li>
<li>컨테이너가 삭제되어도 <strong>볼륨은 유지되며, 데이터가 보존된다.</strong></li>
</ul>
<h4 id="사용-예시-1">사용 예시</h4>
<pre><code class="language-sh">docker volume create my-volume

docker run -d --name volume-container \
  -v my-volume:/app \
  nginx</code></pre>
<ul>
<li><p><code>my-volume</code>이라는 Docker 볼륨을 생성하고 컨테이너의 <code>/app</code>에 마운트한다.</p>
</li>
<li><p>컨테이너가 종료되더라도 <strong>볼륨 데이터는 유지된다.</strong></p>
</li>
<li><p>볼륨을 삭제하려면 명시적으로 삭제해야 한다:</p>
<pre><code class="language-sh">docker volume rm my-volume</code></pre>
</li>
</ul>
<hr>
<h4 id="✅-3-바인드-마운트-vs-볼륨-비교">✅ 3. 바인드 마운트 vs 볼륨 비교</h4>
<table>
<thead>
<tr>
<th>구분</th>
<th>바인드 마운트 (Bind Mount)</th>
<th>볼륨 (Volume)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>저장 위치</strong></td>
<td>호스트의 특정 디렉터리</td>
<td>Docker가 관리하는 <code>/var/lib/docker/volumes/</code></td>
</tr>
<tr>
<td><strong>데이터 유지</strong></td>
<td>컨테이너 삭제 후에도 유지 (호스트 파일 유지됨)</td>
<td>컨테이너 삭제 후에도 유지 (볼륨 삭제 전까지)</td>
</tr>
<tr>
<td><strong>독립성</strong></td>
<td>호스트 파일 시스템과 강하게 결합</td>
<td>Docker가 관리, 컨테이너와 독립적</td>
</tr>
<tr>
<td><strong>사용 목적</strong></td>
<td><strong>로컬 개발 환경, 소스 코드 공유</strong></td>
<td><strong>데이터베이스, 로그 저장, 여러 컨테이너 간 데이터 공유</strong></td>
</tr>
<tr>
<td><strong>성능</strong></td>
<td>호스트 파일 시스템에 의존 → 속도 차이가 있음</td>
<td>Docker 내부에서 최적화되어 빠름</td>
</tr>
</tbody></table>
<hr>
<h4 id="✅-4-바인드-마운트와-볼륨의-사용-사례">✅ 4. 바인드 마운트와 볼륨의 사용 사례</h4>
<h3 id="바인드-마운트를-사용할-경우">바인드 마운트를 사용할 경우</h3>
<ul>
<li>개발 환경에서 호스트 파일을 컨테이너와 동기화해야 할 때  </li>
<li>호스트의 소스 코드 변경 사항이 컨테이너에 즉시 반영된다.</li>
<li>호스트의 특정 파일을 컨테이너에서 직접 사용해야 할 때</li>
</ul>
<h3 id="볼륨을-사용할-경우">볼륨을 사용할 경우</h3>
<ul>
<li>데이터베이스 컨테이너에서 데이터를 영구 저장해야 할 때</li>
<li>컨테이너가 삭제되더라도 데이터를 유지하고 싶은 경우</li>
<li>여러 컨테이너에서 같은 데이터를 공유해야 할 경우</li>
</ul>
<blockquote>
</blockquote>
<p><strong>운영 환경</strong>에서는 일반적으로 <strong>볼륨</strong>을 사용하며,<br><strong>개발 환경</strong>에서는 <strong>바인드 마운트를 활용</strong>하는 것이 편리하다.</p>
]]></description>
        </item>
    </channel>
</rss>