<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>cokke_bear</title>
        <link>https://velog.io/</link>
        <description>back end developer</description>
        <lastBuildDate>Tue, 25 Nov 2025 00:28:52 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>cokke_bear</title>
            <url>https://velog.velcdn.com/images/ch_kang/profile/cf522e36-2851-4ba8-81a7-2591b441647c/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. cokke_bear. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ch_kang" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[회원가입 없이 쓰는 실시간 파일 공유 서비스]]></title>
            <link>https://velog.io/@ch_kang/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%97%86%EC%9D%B4-%EC%93%B0%EB%8A%94-%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%8C%8C%EC%9D%BC-%EA%B3%B5%EC%9C%A0-%EC%84%9C%EB%B9%84%EC%8A%A4</link>
            <guid>https://velog.io/@ch_kang/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%97%86%EC%9D%B4-%EC%93%B0%EB%8A%94-%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%8C%8C%EC%9D%BC-%EA%B3%B5%EC%9C%A0-%EC%84%9C%EB%B9%84%EC%8A%A4</guid>
            <pubDate>Tue, 25 Nov 2025 00:28:52 GMT</pubDate>
            <description><![CDATA[<h2 id="httpswwwclipboardapporg"><a href="https://www.clipboardapp.org/">https://www.clipboardapp.org/</a></h2>
<h2 id="📌-왜-만들었나요">📌 왜 만들었나요?</h2>
<p>수업 중에 교수님이 자료를 공유하려고 할 때, 회의에서 급하게 파일을 전달해야 할 때, PC에서 스마트폰으로 사진을 옮기고 싶을 때... 여러분은 어떤 방법을 사용하시나요?</p>
<p><strong>기존 파일 공유 방법의 문제점:</strong></p>
<ul>
<li><strong>이메일</strong>: 로그인 필요, 용량 제한, 느린 전송 속도</li>
<li><strong>메신저</strong> (카카오톡, 텔레그램): 본인 계정에 보내기 번거로움, 압축으로 화질 저하</li>
<li><strong>클라우드</strong> (Google Drive, Dropbox): 회원가입 필수, 앱 설치 필요, 권한 설정 복잡</li>
<li><strong>USB/AirDrop</strong>: 물리적 접근 필요, iOS-Windows 간 불가능</li>
</ul>
<p>이런 불편함을 해결하기 위해 <strong>회원가입 없이 바로 사용할 수 있는 실시간 파일 공유 서비스</strong>를 만들었습니다.</p>
<hr>
<h2 id="✨-핵심-기능-비로그인-시스템">✨ 핵심 기능: 비로그인 시스템</h2>
<p>이 프로젝트의 가장 큰 차별점은 <strong>완전한 비로그인 시스템</strong>입니다.</p>
<h3 id="🚀-사용-방법-3단계면-끝">🚀 사용 방법 (3단계면 끝!)</h3>
<ol>
<li><strong>웹사이트 접속</strong> → 자동으로 6자리 룸 번호 생성 (예: 123456)</li>
<li><strong>룸 번호 공유</strong> → QR 코드 또는 번호 전달</li>
<li><strong>파일 공유 시작</strong> → 드래그 앤 드롭 또는 Ctrl+V로 업로드</li>
</ol>
<p><strong>계정 생성, 로그인, 앱 설치 전혀 필요 없습니다.</strong></p>
<h3 id="📱-qr-코드-지원">📱 QR 코드 지원</h3>
<p>모바일 접근성을 높이기 위해 QR 코드 기능을 지원합니다:</p>
<ul>
<li>PC에서 룸 생성 → QR 코드 표시</li>
<li>스마트폰으로 QR 스캔 → 즉시 룸 입장</li>
<li>PC-모바일 간 파일 전송이 5초 만에 완료</li>
</ul>
<hr>
<h2 id="🎯-실제-사용-시나리오">🎯 실제 사용 시나리오</h2>
<h3 id="시나리오-1-대학-강의">시나리오 1: 대학 강의</h3>
<pre><code>👨‍🏫 교수님: 웹사이트 접속 → 룸 생성 (예: 456789)
             → QR 코드를 빔프로젝터로 화면 공유

👨‍🎓 학생들: QR 코드 스캔 → 즉시 룸 입장
             → 실시간으로 강의자료 수신</code></pre><h3 id="시나리오-2-회의-중-자료-공유">시나리오 2: 회의 중 자료 공유</h3>
<pre><code>💼 발표자: 룸 번호를 채팅에 공유 (123456)
👥 참석자: 번호 입력 → 입장
📄 파일: 실시간 동기화, 모든 참석자가 동시에 수신</code></pre><h3 id="시나리오-3-pc-모바일-파일-전송">시나리오 3: PC-모바일 파일 전송</h3>
<pre><code>💻 PC: 룸 생성 → QR 코드 표시
📱 스마트폰: QR 스캔 → 사진 업로드
💻 PC: 실시간으로 사진 수신 → 바로 사용</code></pre><hr>
<h2 id="🛠️-기술-스택-개발자-분들을-위해">🛠️ 기술 스택 (개발자 분들을 위해)</h2>
<h3 id="backend">Backend</h3>
<ul>
<li><strong>Node.js + TypeScript</strong>: 타입 안전성을 갖춘 서버</li>
<li><strong>Socket.IO</strong>: 실시간 양방향 통신 (WebSocket)</li>
<li><strong>Express</strong>: HTTP 서버</li>
<li><strong>Jest</strong>: TDD(Test-Driven Development) 방식으로 개발</li>
</ul>
<h3 id="frontend">Frontend</h3>
<ul>
<li><strong>Vue 3</strong>: 반응형 UI 프레임워크</li>
<li><strong>Vite</strong>: 빠른 빌드 도구</li>
<li><strong>Tailwind CSS</strong>: 유틸리티 기반 스타일링</li>
<li><strong>Socket.IO Client</strong>: 실시간 통신</li>
</ul>
<h3 id="핵심-아키텍처">핵심 아키텍처</h3>
<pre><code>클라이언트 A ←→ Socket.IO Server ←→ 클라이언트 B
                    ↓
               RoomManager
         (메모리 기반 룸 관리)</code></pre><hr>
<h2 id="🔐-보안--프라이버시">🔐 보안 &amp; 프라이버시</h2>
<p><strong>데이터 보관 정책:</strong></p>
<ul>
<li>파일은 서버 메모리에만 임시 저장</li>
<li>마지막 사용자가 퇴장하면 룸과 파일 즉시 삭제</li>
<li>데이터베이스에 저장하지 않음 (완전한 휘발성)</li>
<li>사용자 정보 수집 전혀 없음</li>
</ul>
<p><strong>보안 기능:</strong></p>
<ul>
<li>CORS 설정으로 허용된 도메인만 접근</li>
<li>파일 크기 제한 (서버 과부하 방지)</li>
<li>파일 타입 검증 (악성 파일 차단 예정)</li>
</ul>
<hr>
<h2 id="📊-프로덕션-최적화">📊 프로덕션 최적화</h2>
<p>프로덕션 환경을 위한 최적화도 완료했습니다:</p>
<h3 id="frontend-1">Frontend</h3>
<ul>
<li>✅ Terser를 이용한 console.log 완전 제거</li>
<li>✅ 코드 스플리팅 (vendor/socket 청크 분리)</li>
<li>✅ Gzip 압축으로 6.4% 용량 절감</li>
</ul>
<h3 id="backend-1">Backend</h3>
<ul>
<li>✅ 환경별 Logger 유틸리티 (프로덕션에서 디버그 로그 비활성화)</li>
<li>✅ Docker 멀티 스테이지 빌드 (이미지 크기 30-50% 감소)</li>
<li>✅ Graceful shutdown 처리</li>
</ul>
<hr>
<h2 id="🚀-향후-계획">🚀 향후 계획</h2>
<p>현재는 MVP(Minimum Viable Product) 단계이며, 다음 기능을 개발 중입니다:</p>
<p><strong>Phase 1: 핵심 기능 강화</strong></p>
<ul>
<li><input disabled="" type="checkbox"> 파일 크기 제한 및 예외 처리 강화</li>
<li><input disabled="" type="checkbox"> 드래그 앤 드롭 UI 추가</li>
<li><input disabled="" type="checkbox"> 다중 파일 ZIP 다운로드</li>
<li><input disabled="" type="checkbox"> 파일 미리보기 기능</li>
</ul>
<p><strong>Phase 2: 사용성 개선</strong></p>
<ul>
<li><input disabled="" type="checkbox"> 텍스트 메시지 공유</li>
<li><input disabled="" type="checkbox"> 업로드/다운로드 진행률 표시</li>
<li><input disabled="" type="checkbox"> 닉네임 설정</li>
<li><input disabled="" type="checkbox"> 다크 모드</li>
</ul>
<p><strong>Phase 3: 고급 기능</strong></p>
<ul>
<li><input disabled="" type="checkbox"> 룸 비밀번호 설정 (선택적 보안)</li>
<li><input disabled="" type="checkbox"> 파일 만료 시간 설정</li>
<li><input disabled="" type="checkbox"> 모바일 최적화</li>
</ul>
<hr>
<h2 id="🎓-개발-과정에서-배운-것">🎓 개발 과정에서 배운 것</h2>
<h3 id="1-tdd의-중요성">1. TDD의 중요성</h3>
<p>모든 Socket.IO 이벤트를 테스트 코드로 먼저 작성했습니다. 덕분에 리팩토링 시 안정성을 보장할 수 있었습니다.</p>
<h3 id="2-실시간-통신의-복잡성">2. 실시간 통신의 복잡성</h3>
<ul>
<li>연결 끊김 처리 (Connection State Recovery)</li>
<li>타임아웃 설정</li>
<li>에러 핸들링의 중요성</li>
</ul>
<h3 id="3-사용자-경험-최우선">3. 사용자 경험 최우선</h3>
<p>기술적으로 완벽한 것보다, <strong>사용자가 5초 안에 이해하고 사용할 수 있는</strong> 서비스를 만드는 것이 더 중요했습니다.</p>
<hr>
<h2 id="🔗-직접-사용해보세요">🔗 직접 사용해보세요!</h2>
<h2 id="서비스-웹사이트-url"><strong>서비스</strong>: <a href="https://www.clipboardapp.org/">[웹사이트 URL]</a></h2>
<p><strong>GitHub</strong>: [<a href="https://github.com/Kangchanghwan/only_ai_project">저장소 URL</a></p>
<p>오픈소스 프로젝트이므로 누구나 자유롭게 사용하고 기여할 수 있습니다.
이슈 및 풀 리퀘스트는 언제나 환영합니다!</p>
<hr>
<h2 id="💬-마치며">💬 마치며</h2>
<p>&quot;<strong>회원가입 없이 바로 쓸 수 있다</strong>&quot;는 단순한 컨셉 하나로 시작한 프로젝트입니다.</p>
<p>복잡한 권한 설정, 계정 관리, 로그인 과정 없이 <strong>URL 하나면 충분한</strong> 세상을 꿈꿉니다. 이 서비스가 여러분의 일상에서 작은 불편함을 해소해주길 바랍니다.</p>
<p>궁금한 점이나 개선 아이디어가 있으시면 언제든 GitHub 이슈로 남겨주세요!</p>
<hr>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/ca104196-2bbc-4748-ac73-f2d1b35cbf7d/image.png" alt="">
<img src="blob:https://velog.io/cad2cbc0-bc6e-4b4b-968a-620c90eb1fb4" alt="업로드중.."></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD] 언리얼 젠킨스 자동패키징]]></title>
            <link>https://velog.io/@ch_kang/CICD-%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%A0%A0%ED%82%A8%EC%8A%A4-%EC%9E%90%EB%8F%99%ED%8C%A8%ED%82%A4%EC%A7%95</link>
            <guid>https://velog.io/@ch_kang/CICD-%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%A0%A0%ED%82%A8%EC%8A%A4-%EC%9E%90%EB%8F%99%ED%8C%A8%ED%82%A4%EC%A7%95</guid>
            <pubDate>Tue, 04 Nov 2025 00:18:32 GMT</pubDate>
            <description><![CDATA[<ul>
<li>구축 이유는 언리얼개발만 진행해 봤지 빌드까지 해본적은 없었다. 더군다나 젠킨스를 활용하여 자동 빌드 구성을 구축하는 것은 새로운 경험이라고 생각했었다. </li>
<li>시나리오는 간단하게 로컬에 젠킨스를 구축하고 언리얼 코드 변경 후 커밋/푸시를 진행한다.</li>
<li>젠킨스에서 수동으로 빌드버튼을 눌러 빌드를 실행해 보았다.</li>
</ul>
<h1 id="결과">결과</h1>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/8b399d70-2654-4953-8ab7-d3bab5d904bd/image.png" alt="">
<img src="https://velog.velcdn.com/images/ch_kang/post/bc14e88c-ea25-413a-9e33-6a7b96ca298c/image.png" alt=""></p>
<h2 id="아키텍쳐">아키텍쳐</h2>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/cd74e7ce-6ac8-42df-b5fa-de0d8ba56459/image.png" alt=""></p>
<h2 id="고민">고민</h2>
<ul>
<li>웹서버에서 자동으로 ci/cd 구축으로 경험하였던 깃 웹훅은 사용할 수 있는 방법은 무엇인지 고민해 보아야겠다. 이전 웹서버로 구축했을 때는 가벼운 성능으로도 빌드가 가능했지만, 언리얼 빌드는 복잡하고 용량이 크기때문에 EC2서버로 구축하기에는 비용부담이 컸다. </li>
<li>개임 개발의 경우 내부망으로 구축하는 경우가 대부분이기 때문에 Perforce 스트림을 고려한다면 빌드 매니저라는 직군의 필요성이 느껴 졌다.</li>
</ul>
<h2 id="참고문서">참고문서</h2>
<p><a href="https://dev.epicgames.com/documentation/ko-kr/unreal-engine/packaging-your-project">https://dev.epicgames.com/documentation/ko-kr/unreal-engine/packaging-your-project</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(ISTQB) 자격증 정리 문서]]></title>
            <link>https://velog.io/@ch_kang/ISTQB-%EC%9E%90%EA%B2%A9%EC%A6%9D-%EC%A0%95%EB%A6%AC-%EB%AC%B8%EC%84%9C</link>
            <guid>https://velog.io/@ch_kang/ISTQB-%EC%9E%90%EA%B2%A9%EC%A6%9D-%EC%A0%95%EB%A6%AC-%EB%AC%B8%EC%84%9C</guid>
            <pubDate>Wed, 22 Oct 2025 05:13:49 GMT</pubDate>
            <description><![CDATA[<h1 id="테스트-프로세스-관계-기반-통합-정리">테스트 프로세스 관계 기반 통합 정리</h1>
<p>본 문서는 ISTQB CTFL v4.0 실러버스의 핵심 내용을 <strong>관계 중심</strong>으로 재구성한 것입니다. 각 항목의 상세 내용은 명시된 장/절을 참조하시기 바랍니다.</p>
<hr>
<h2 id="1-테스팅의-기본-개념과-목적-기초-제1장">1. 테스팅의 기본 개념과 목적 (기초: 제1장)</h2>
<h3 id="11-테스팅의-정의와-범위-상세-11절"><strong>1.1 테스팅의 정의와 범위</strong> (상세: 1.1절)</h3>
<h4 id="테스팅이란">테스팅이란?</h4>
<ul>
<li><strong>결함 식별과 품질 평가</strong>를 위한 일련의 활동</li>
<li>테스트 대상(test object): 테스트되는 모든 작업 산출물</li>
<li><strong>베리피케이션</strong>(요구사항 충족 확인) + <strong>밸리데이션</strong>(사용자 필요 충족 확인)</li>
<li><strong>동적 테스팅</strong>(실행) + <strong>정적 테스팅</strong>(비실행)</li>
</ul>
<h4 id="테스팅과-디버깅의-차이-상세-112절">테스팅과 디버깅의 차이 (상세: 1.1.2절)</h4>
<ul>
<li><strong>테스팅</strong>: 장애 유발 또는 결함 발견</li>
<li><strong>디버깅</strong>: 장애 원인 찾기 → 분석 → 제거</li>
<li><strong>관계</strong>: 테스팅에서 발견한 결함을 디버깅으로 수정 → <strong>확인 테스팅</strong>으로 검증 → <strong>리그레션 테스팅</strong>으로 부작용 확인 <strong>(2.2.3절 참조)</strong></li>
</ul>
<h4 id="일반적인-테스트-목적-상세-111절">일반적인 테스트 목적 (상세: 1.1.1절)</h4>
<ol>
<li>작업 산출물 평가</li>
<li>장애 유발 및 결함 식별</li>
<li>요구된 커버리지 보장 <strong>(4장 테스트 기법 참조)</strong></li>
<li>리스크 수준 완화 <strong>(5.2절 리스크 관리 참조)</strong></li>
<li>베리피케이션 및 밸리데이션</li>
<li>이해관계자 의사결정 지원 <strong>(5.3절 테스트 보고서 참조)</strong></li>
<li>품질에 대한 자신감 획득</li>
</ol>
<hr>
<h3 id="12-테스팅이-필요한-이유-상세-12절"><strong>1.2 테스팅이 필요한 이유</strong> (상세: 1.2절)</h3>
<h4 id="테스팅의-가치">테스팅의 가치</h4>
<ul>
<li><strong>조기 결함 발견</strong>으로 비용 절감 → <strong>조기 테스팅 원리 (1.3절)</strong> 및 <strong>시프트 레프트 (2.1.5절)</strong> 연결</li>
<li><strong>품질 직접 평가</strong> → <strong>테스트 메트릭 (5.3.1절)</strong> 활용</li>
<li><strong>사용자 대변</strong> → <strong>인수 테스팅 (2.2.1절)</strong> 연결</li>
<li>계약/법률/규제 요구사항 충족</li>
</ul>
<h4 id="테스팅과-품질-보증qa의-관계-상세-122절">테스팅과 품질 보증(QA)의 관계 (상세: 1.2.2절)</h4>
<ul>
<li><strong>테스팅</strong>: 제품 지향적, 교정 접근법 (품질 제어의 주요 활동)</li>
<li><strong>품질 보증</strong>: 프로세스 지향적, 예방 접근법</li>
<li><strong>상호작용</strong>: 테스트 결과 → QA에 피드백 제공 → 프로세스 개선 → <strong>회고 (2.1.6절)</strong> 연결</li>
</ul>
<h4 id="오류-결함-장애-근본-원인-관계도-상세-123절">오류-결함-장애-근본 원인 관계도 (상세: 1.2.3절)</h4>
<pre><code>근본 원인 → 오류(실수) → 결함(버그) → 장애(failure)
                                          ↓
                              테스팅으로 발견 → 디버깅 → 수정</code></pre><hr>
<h3 id="13-테스팅의-7가지-원리-상세-13절"><strong>1.3 테스팅의 7가지 원리</strong> (상세: 1.3절)</h3>
<ol>
<li><strong>테스팅은 결함 존재를 밝히지만, 부재를 증명하지 않는다</strong></li>
<li><strong>완벽한 테스팅은 불가능하다</strong> → <strong>테스트 기법 (4장)</strong>, <strong>우선순위지정 (5.1.5절)</strong>, <strong>리스크 기반 (5.2절)</strong> 활용</li>
<li><strong>조기 테스팅으로 시간과 비용 절약</strong> → <strong>시프트 레프트 (2.1.5절)</strong>, <strong>정적 테스팅 (3장)</strong> 연결</li>
<li><strong>결함은 집중된다</strong> → <strong>리스크 기반 테스팅 (5.2절)</strong> 적용</li>
<li><strong>테스트 효과는 줄어든다</strong> → <strong>리그레션 테스트 자동화 (2.2.3절, 6장)</strong> 필요</li>
<li><strong>테스팅은 정황에 의존적이다</strong> → <strong>SDLC별 조정 (2.1절)</strong>, <strong>테스트 계획 (5.1절)</strong></li>
<li><strong>결함-부재는 궤변이다</strong> → <strong>베리피케이션 + 밸리데이션</strong> 모두 필요</li>
</ol>
<hr>
<h2 id="2-테스트-프로세스와-활동의-흐름-제1장--제5장-통합">2. 테스트 프로세스와 활동의 흐름 (제1장 + 제5장 통합)</h2>
<h3 id="21-테스트-프로세스-전체-흐름-상세-141절"><strong>2.1 테스트 프로세스 전체 흐름</strong> (상세: 1.4.1절)</h3>
<pre><code>[계획] → [모니터링/제어] → [분석] → [설계] → [구현] → [실행] → [완료]
   ↓         ↓              ↓        ↓         ↓         ↓        ↓
 5.1절    5.3절         4장 적용  4장 적용  5.1.5우선순위 5.5결함관리 2.1.6회고</code></pre><h4 id="각-활동의-핵심과-상호관계">각 활동의 핵심과 상호관계</h4>
<p><strong>1) 테스트 계획</strong> (상세: 1.4.1, 5.1절)</p>
<ul>
<li>목적 정의 및 접근법 선택</li>
<li><strong>산출물</strong>: 테스트 계획서 (5.1.1), 테스트 전략</li>
<li><strong>연관</strong>: <ul>
<li>리스크 관리 (5.2절) 결과 반영</li>
<li>추정 기법 (5.1.4절) 사용</li>
<li>시작/완료 조건 (5.1.3절) 정의</li>
<li>SDLC 유형 (2.1절) 고려</li>
</ul>
</li>
</ul>
<p><strong>2) 테스트 모니터링과 제어</strong> (상세: 1.4.1, 5.3절)</p>
<ul>
<li><strong>모니터링</strong>: 진행 상황 지속 점검</li>
<li><strong>제어</strong>: 목적 달성 위한 조치</li>
<li><strong>산출물</strong>: 테스트 진행 상황 보고서 (5.3.2)</li>
<li><strong>연관</strong>:<ul>
<li>테스트 메트릭 (5.3.1) 수집</li>
<li>리스크 모니터링 (5.2.4) 연계</li>
<li>형상 관리 (5.4절) 활용</li>
</ul>
</li>
</ul>
<p><strong>3) 테스트 분석</strong> (상세: 1.4.1, 4장)</p>
<ul>
<li>&quot;<strong>무엇을 테스트할 것인가?</strong>&quot;</li>
<li>테스트 베이시스 분석 → 테스트 컨디션 정의</li>
<li><strong>연관</strong>:<ul>
<li>테스트 기법 (4장) 사용</li>
<li>리스크 분석 (5.2.3) 결과 반영</li>
<li>추적성 (1.4.4) 구축 시작</li>
</ul>
</li>
</ul>
<p><strong>4) 테스트 설계</strong> (상세: 1.4.1, 4장)</p>
<ul>
<li>&quot;<strong>어떻게 테스트할 것인가?</strong>&quot;</li>
<li>테스트 컨디션 → 테스트 케이스 구체화</li>
<li><strong>산출물</strong>: 테스트 케이스, 테스트 차터, 커버리지 항목</li>
<li><strong>연관</strong>:<ul>
<li>테스트 기법 (4.2~4.5) 적용</li>
<li>테스트 데이터 요구사항 정의</li>
<li>테스트 환경 (1.4.3) 설계</li>
</ul>
</li>
</ul>
<p><strong>5) 테스트 구현</strong> (상세: 1.4.1)</p>
<ul>
<li>테스트 실행 준비</li>
<li><strong>산출물</strong>: 테스트 절차, 테스트 스위트, 테스트 스크립트, 테스트 실행 일정</li>
<li><strong>연관</strong>:<ul>
<li>우선순위지정 (5.1.5) 적용</li>
<li>테스트 자동화 (6장) 구현</li>
<li>테스트 환경 구축</li>
</ul>
</li>
</ul>
<p><strong>6) 테스트 실행</strong> (상세: 1.4.1)</p>
<ul>
<li>테스트 실행 및 결과 비교</li>
<li><strong>산출물</strong>: 테스트 로그, 결함 보고서</li>
<li><strong>연관</strong>:<ul>
<li>결함 관리 (5.5절) 프로세스</li>
<li>확인 테스팅/리그레션 테스팅 (2.2.3)</li>
<li>테스트 도구 (6.1절) 활용</li>
</ul>
</li>
</ul>
<p><strong>7) 테스트 완료</strong> (상세: 1.4.1, 5.3절)</p>
<ul>
<li>마일스톤에서 수행</li>
<li><strong>산출물</strong>: 테스트 완료 보고서 (5.3.2), 교훈, 변경 요청서</li>
<li><strong>연관</strong>:<ul>
<li>회고 (2.1.6) 수행</li>
<li>테스트웨어 보관</li>
<li>프로세스 개선</li>
</ul>
</li>
</ul>
<hr>
<h3 id="22-테스트웨어와-추적성-상세-143-144절"><strong>2.2 테스트웨어와 추적성</strong> (상세: 1.4.3, 1.4.4절)</h3>
<h4 id="테스트웨어의-종류와-활동별-연계">테스트웨어의 종류와 활동별 연계</h4>
<pre><code>활동          → 주요 테스트웨어
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
계획          → 테스트 계획서, 리스크 관리 대장 (5.2)
모니터링/제어 → 테스트 진행 보고서 (5.3.2), 제어 지침
분석          → 테스트 컨디션, 결함 보고서
설계          → 테스트 케이스, 테스트 차터, 커버리지 항목
구현          → 테스트 절차, 테스트 스크립트, 테스트 데이터
실행          → 테스트 로그, 결함 보고서 (5.5)
완료          → 테스트 완료 보고서 (5.3.2), 교훈</code></pre><h4 id="추적성의-가치-상세-144절">추적성의 가치 (상세: 1.4.4절)</h4>
<ul>
<li><strong>커버리지 평가</strong> 지원: 테스트 케이스 ↔ 요구사항</li>
<li><strong>리스크 평가</strong>: 테스트 결과 ↔ 리스크 (5.2절)</li>
<li><strong>영향도 분석</strong>: 변경 → 리그레션 테스트 범위 (2.2.3)</li>
<li><strong>형상 관리</strong> (5.4절) 지원</li>
<li><strong>테스트 보고</strong> (5.3.2절) 명확화</li>
</ul>
<hr>
<h3 id="23-테스팅-역할과-책임-상세-145절"><strong>2.3 테스팅 역할과 책임</strong> (상세: 1.4.5절)</h3>
<h4 id="두-가지-주요-역할">두 가지 주요 역할</h4>
<p><strong>테스트 관리 역할</strong></p>
<ul>
<li><strong>책임</strong>: 테스트 프로세스, 팀, 활동 리더십</li>
<li><strong>주요 활동</strong>: <ul>
<li>테스트 계획 (5.1)</li>
<li>테스트 모니터링/제어 (5.3)</li>
<li>테스트 완료</li>
</ul>
</li>
<li><strong>연관</strong>: 리스크 관리 (5.2), 테스트 보고 (5.3.2)</li>
</ul>
<p><strong>테스팅 역할</strong></p>
<ul>
<li><strong>책임</strong>: 테스팅의 공학적 측면</li>
<li><strong>주요 활동</strong>:<ul>
<li>테스트 분석</li>
<li>테스트 설계</li>
<li>테스트 구현</li>
<li>테스트 실행</li>
</ul>
</li>
<li><strong>연관</strong>: 테스트 기법 (4장), 결함 관리 (5.5)</li>
</ul>
<hr>
<h3 id="24-테스팅-필수-기술과-협업-상세-15절"><strong>2.4 테스팅 필수 기술과 협업</strong> (상세: 1.5절)</h3>
<h4 id="보편적으로-필요한-기술-상세-151절">보편적으로 필요한 기술 (상세: 1.5.1절)</h4>
<ul>
<li><strong>테스팅 지식</strong>: 테스트 기법 (4장) 활용</li>
<li><strong>철저함/신중함</strong>: 결함 식별</li>
<li><strong>의사소통 기술</strong>: 결함 보고 (5.5), 테스트 보고 (5.3.2)</li>
<li><strong>분석/비판적 사고</strong>: 테스트 설계, 리스크 분석 (5.2)</li>
<li><strong>기술/도메인 지식</strong>: 도구 사용 (6장), 요구사항 이해</li>
</ul>
<h4 id="전체-팀-접근법-상세-152절">전체 팀 접근법 (상세: 1.5.2절)</h4>
<ul>
<li><strong>개념</strong>: 팀원 누구나 작업 수행 가능, 모두가 품질 책임</li>
<li><strong>연관</strong>:<ul>
<li>협업 기반 테스트 접근법 (4.5절)</li>
<li>애자일 개발 (2.1절)</li>
<li>리뷰 (3.2절) 참여</li>
</ul>
</li>
</ul>
<h4 id="테스팅의-독립성-상세-153절">테스팅의 독립성 (상세: 1.5.3절)</h4>
<ul>
<li><strong>독립성 수준</strong>: 없음 → 일정 → 높음 → 매우 높음</li>
<li><strong>장점</strong>: 다른 관점으로 결함 식별, 가정 검증</li>
<li><strong>단점</strong>: 협업 저해 가능, 책임감 손실 가능</li>
<li><strong>연관</strong>: <ul>
<li>테스트 레벨별 역할 (2.2.1)</li>
<li>리뷰 역할 (3.2.3)</li>
<li>안전 치명적 시스템 요구사항</li>
</ul>
</li>
</ul>
<hr>
<h2 id="3-sdlc와-테스팅의-관계-제2장">3. SDLC와 테스팅의 관계 (제2장)</h2>
<h3 id="31-sdlc가-테스팅에-미치는-영향-상세-211절"><strong>3.1 SDLC가 테스팅에 미치는 영향</strong> (상세: 2.1.1절)</h3>
<h4 id="sdlc-모델별-테스팅-특성">SDLC 모델별 테스팅 특성</h4>
<pre><code>SDLC 유형         → 테스팅 특성                  → 관련 개념
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
순차적 개발       → 초기 리뷰, 후반 동적 테스팅   → 테스트 레벨 (2.2.1)
반복적/점진적     → 모든 반복마다 전체 테스트      → 리그레션 테스팅 (2.2.3)
애자일           → 가벼운 산출물, 자동화 선호    → 테스트 자동화 (6장)
                                               → 경험 기반 기법 (4.4)</code></pre><h3 id="32-sdlc의-좋은-테스팅-프랙티스-상세-212절"><strong>3.2 SDLC의 좋은 테스팅 프랙티스</strong> (상세: 2.1.2절)</h3>
<ol>
<li><strong>개발 활동 ↔ 테스트 활동 매핑</strong> → 품질 제어 대상</li>
<li><strong>테스트 레벨별 독립 목적</strong> (2.2.1절) → 중복 방지</li>
<li><strong>조기 테스트 시작</strong> → 시프트 레프트 (2.1.5절)</li>
<li><strong>리뷰 조기 참여</strong> (3장) → 시프트 레프트</li>
</ol>
<hr>
<h3 id="33-테스트-주도-개발-접근법-상세-213절"><strong>3.3 테스트 주도 개발 접근법</strong> (상세: 2.1.3절)</h3>
<h4 id="세-가지-접근법의-관계">세 가지 접근법의 관계</h4>
<pre><code>TDD (테스트 주도 개발)
  └─ 컴포넌트 레벨 (2.2.1)
  └─ 코드 작성 전 테스트 작성
  └─ 화이트박스 기법 (4.3) 활용

ATDD (인수 테스트 주도 개발) ←→ 4.5.3절 상세
  └─ 시스템/인수 레벨 (2.2.1)
  └─ 인수 조건 (4.5.2) 기반
  └─ 협업 기반 접근법 (4.5)

BDD (행위 주도 개발)
  └─ Given/When/Then 형식
  └─ 이해관계자 이해 용이
  └─ ATDD와 유사</code></pre><p><strong>공통점</strong>: </p>
<ul>
<li>테스트 우선 (조기 테스팅 원리 1.3)</li>
<li>시프트 레프트 (2.1.5)</li>
<li>자동화 유지 (6장)</li>
</ul>
<hr>
<h3 id="34-devops와-테스팅-상세-214절"><strong>3.4 DevOps와 테스팅</strong> (상세: 2.1.4절)</h3>
<h4 id="devops의-테스팅-영향">DevOps의 테스팅 영향</h4>
<pre><code>DevOps 요소        → 테스팅 연관
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CI/CD             → 자동화 테스트 (6장)
                  → 리그레션 테스트 (2.2.3)
                  → 시프트 레프트 (2.1.5)

배포 파이프라인   → 테스트 자동화 도구 (6.1)
                  → 형상 관리 (5.4)

빠른 피드백       → 테스트 모니터링 (5.3)
                  → 테스트 메트릭 (5.3.1)</code></pre><hr>
<h3 id="35-시프트-레프트-전략-상세-215절"><strong>3.5 시프트 레프트 전략</strong> (상세: 2.1.5절)</h3>
<h4 id="시프트-레프트-실천-방법과-연관">시프트 레프트 실천 방법과 연관</h4>
<ul>
<li><strong>명세 리뷰</strong> → 정적 테스팅 (3장)</li>
<li><strong>테스트 우선 작성</strong> → TDD/ATDD (2.1.3)</li>
<li><strong>CI/CD 적용</strong> → DevOps (2.1.4)</li>
<li><strong>정적 분석</strong> → 정적 테스팅 (3.1)</li>
<li><strong>조기 비기능 테스팅</strong> → 컴포넌트 레벨부터 (2.2.1)</li>
</ul>
<p><strong>효과</strong>: 후반 비용 절감 (1.3 조기 테스팅 원리)</p>
<hr>
<h3 id="36-회고와-프로세스-개선-상세-216절"><strong>3.6 회고와 프로세스 개선</strong> (상세: 2.1.6절)</h3>
<h4 id="회고의-통합적-역할">회고의 통합적 역할</h4>
<pre><code>회고 시점          → 연관 활동
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
프로젝트 종료     → 테스트 완료 (1.4.1)
반복 주기 종료    → 스프린트 회고 (애자일)
릴리스 마일스톤   → 테스트 완료 보고서 (5.3.2)</code></pre><h4 id="테스팅-관점의-회고-이점">테스팅 관점의 회고 이점</h4>
<ul>
<li><strong>효과성/효율성 향상</strong> → 프로세스 개선 → 테스트 계획 (5.1) 반영</li>
<li><strong>테스트웨어 품질</strong> → 형상 관리 (5.4) 개선</li>
<li><strong>팀 결속/학습</strong> → 전체 팀 접근법 (1.5.2)</li>
<li><strong>테스트 베이시스 개선</strong> → 요구사항 품질 → 테스트 분석 용이</li>
<li><strong>협업 개선</strong> → 리뷰 (3.2), 협업 기반 접근법 (4.5)</li>
</ul>
<hr>
<h2 id="4-테스트-레벨과-테스트-유형-제2장">4. 테스트 레벨과 테스트 유형 (제2장)</h2>
<h3 id="41-테스트-레벨의-계층과-관계-상세-221절"><strong>4.1 테스트 레벨의 계층과 관계</strong> (상세: 2.2.1절)</h3>
<pre><code>레벨                  → 주요 목적           → 테스트 유형 연관      → 담당자
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
컴포넌트 테스트       → 개별 컴포넌트       → 화이트박스 (4.3)     → 개발자
                                           → 1사분면 (5.1.7)      → 자동화/CI

컴포넌트 통합        → 인터페이스/상호작용  → 블랙박스/화이트박스  → 개발자
                                           → 1사분면             → 통합 전략

시스템 테스트        → 전체 시스템         → 기능 테스트         → 독립 테스터
                    → 엔드투엔드 동작     → 비기능 테스트        → 2,4사분면 (5.1.7)

시스템 통합          → 외부 시스템 연계    → 블랙박스           → 독립 테스터
                                          → API 테스팅          → 운영 환경 유사

인수 테스트          → 배포 준비 확인      → 밸리데이션          → 실제 사용자
                    → 비즈니스 요구 충족  → 3사분면 (5.1.7)     → UAT, 알파/베타</code></pre><h4 id="테스트-레벨-간-관계">테스트 레벨 간 관계</h4>
<ul>
<li><strong>상향식</strong>: 컴포넌트 → 컴포넌트 통합 → 시스템 → 시스템 통합 → 인수</li>
<li><strong>각 레벨의 완료 조건</strong> (5.1.3) → 다음 레벨의 시작 조건</li>
<li><strong>테스트 피라미드</strong> (5.1.6): 하위 레벨 많이 ↔ 상위 레벨 적게</li>
</ul>
<hr>
<h3 id="42-테스트-유형과-기법의-매핑-상세-222절"><strong>4.2 테스트 유형과 기법의 매핑</strong> (상세: 2.2.2절)</h3>
<pre><code>테스트 유형         → 주요 초점          → 적용 기법          → 관련 품질 특성
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
기능 테스팅         → 무엇을 하는가      → 블랙박스 (4.2)     → 기능 성숙도/정확성

비기능 테스팅       → 얼마나 잘 하는가   → 블랙박스/화이트박스 → ISO 25010 특성
                                        → 경험 기반 (4.4)    → (성능/보안/사용성)

블랙박스 테스팅     → 명세 기반         → 4.2절 기법        → 동작 확인
                                        → 동등분할/경계값    

화이트박스 테스팅   → 구조 기반         → 4.3절 기법        → 내부 구조 커버리지
                                        → 구문/분기         

경험 기반 테스팅    → 테스터 지식       → 4.4절 기법        → 블랙/화이트박스 보완
                                        → 탐색적/오류추정    </code></pre><h4 id="모든-레벨에서-모든-유형-가능">모든 레벨에서 모든 유형 가능</h4>
<ul>
<li><strong>컴포넌트 레벨</strong>: 기능 + 비기능(성능) + 화이트박스</li>
<li><strong>시스템 레벨</strong>: 기능 + 비기능(전체) + 블랙박스 + 경험 기반</li>
<li><strong>인수 레벨</strong>: 기능 + 비기능(사용성) + 블랙박스</li>
</ul>
<hr>
<h3 id="43-확인-테스팅과-리그레션-테스팅-상세-223절"><strong>4.3 확인 테스팅과 리그레션 테스팅</strong> (상세: 2.2.3절)</h3>
<h4 id="두-테스팅의-관계와-흐름">두 테스팅의 관계와 흐름</h4>
<pre><code>결함 발견 (테스트 실행) → 디버깅 (수정) → 확인 테스팅 → 리그레션 테스팅
     ↑                                          ↓                ↓
  5.5 결함 관리                        원래 실패 TC 재실행    영향도 분석 후 실행
                                          또는 새 TC 추가      자동화 권장 (6장)</code></pre><h4 id="리그레션-테스팅의-통합적-역할">리그레션 테스팅의 통합적 역할</h4>
<ul>
<li><strong>변경 영향 확인</strong>: 수정, 개선, 환경 변경</li>
<li><strong>영향도 분석</strong>: 테스트 범위 파악 → 추적성 (1.4.4) 활용</li>
<li><strong>자동화 적합</strong>: 반복 실행 → 테스트 자동화 (6장)</li>
<li><strong>CI/CD 통합</strong>: DevOps (2.1.4)</li>
<li><strong>모든 레벨 적용</strong>: 컴포넌트 ~ 인수 테스트</li>
</ul>
<hr>
<h3 id="44-유지보수-테스팅-상세-23절"><strong>4.4 유지보수 테스팅</strong> (상세: 2.3절)</h3>
<h4 id="유지보수-테스팅의-유발요인과-연관">유지보수 테스팅의 유발요인과 연관</h4>
<pre><code>유발요인                   → 테스팅 활동          → 관련 개념
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
계획된 개선/수정/핫픽스     → 변경 검증          → 확인 테스팅 (2.2.3)
                           → 영향도 테스트       → 리그레션 테스팅

환경 업그레이드/마이그레이션 → 환경 테스트        → 시스템 통합 레벨
                           → 데이터 변환 테스트   → 비기능 테스팅

애플리케이션 단종           → 데이터 보관 테스트   → 데이터 무결성
                           → 복원/복구 테스트    → 신뢰성 테스팅</code></pre><h4 id="유지보수-테스팅-범위-결정-요소">유지보수 테스팅 범위 결정 요소</h4>
<ul>
<li><strong>변경 리스크 수준</strong> → 리스크 분석 (5.2.3)</li>
<li><strong>기존 시스템 크기</strong> → 테스트 범위 (5.2.3)</li>
<li><strong>변경사항 크기</strong> → 추정 (5.1.4)</li>
</ul>
<hr>
<h2 id="5-정적-테스팅-제3장">5. 정적 테스팅 (제3장)</h2>
<h3 id="51-정적-테스팅의-역할과-가치-상세-31절"><strong>5.1 정적 테스팅의 역할과 가치</strong> (상세: 3.1절)</h3>
<h4 id="정적-vs-동적-테스팅-비교">정적 vs 동적 테스팅 비교</h4>
<pre><code>구분              정적 테스팅                동적 테스팅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
실행 여부         실행 안 함                 실행 필요
결함 발견         직접 발견                  장애 → 분석 → 결함
적용 대상         모든 작업 산출물            실행 가능 산출물
시기             SDLC 초기 가능             후반 (코드 완성 후)
기법             리뷰, 정적 분석             테스트 케이스 실행</code></pre><h4 id="정적-테스팅과-다른-활동의-연계">정적 테스팅과 다른 활동의 연계</h4>
<ul>
<li><strong>조기 테스팅</strong> (1.3 원리) 구현 → 시프트 레프트 (2.1.5)</li>
<li><strong>결함 예방</strong> → 품질 보증 (1.2.2) 지원</li>
<li><strong>리뷰</strong> (3.2) → 협업 기반 접근법 (4.5) 연결</li>
<li><strong>정적 분석</strong> → CI/CD (2.1.4) 통합</li>
<li><strong>명세 검증</strong> → 테스트 베이시스 품질 → 테스트 분석 (1.4.1) 개선</li>
</ul>
<hr>
<h3 id="52-리뷰-프로세스-상세-32절"><strong>5.2 리뷰 프로세스</strong> (상세: 3.2절)</h3>
<h4 id="리뷰-프로세스-흐름과-연관-활동">리뷰 프로세스 흐름과 연관 활동</h4>
<pre><code>1. 계획
   ├→ 범위/목적 정의
   ├→ 리스크 고려 (5.2절)
   └→ 자원 배정

2. 리뷰 착수
   ├→ 참여자 준비 확인
   └→ 역할 이해 (3.2.3)

3. 개별 리뷰
   ├→ 리뷰 기법 적용
   ├→ 이상 사항 식별
   └→ 체크리스트 활용 가능 (4.4.3)

4. 논의 및 분석
   ├→ 이상 사항 분석
   ├→ 상태/담당자 결정
   └→ 후속 조치 판단

5. 수정 및 보고
   ├→ 결함 보고서 (5.5) 작성
   ├→ 완료 조건 확인
   └→ 리뷰 결과 보고</code></pre><h4 id="리뷰-유형별-특성과-적용">리뷰 유형별 특성과 적용</h4>
<pre><code>유형            공식성   주요 목적                담당      연관
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
비공식 리뷰     낮음    이상 사항 식별           자유      빠른 피드백
워크쓰루       중간    교육/합의/동기부여        저자      협업 (4.5)
기술 리뷰      중간    기술 결정/합의           중재자    전문가 참여
인스펙션       높음    최대 결함 발견/메트릭     리뷰리더  프로세스 개선
                                                        회고 (2.1.6)</code></pre><hr>
<h3 id="53-이해관계자-피드백의-통합-상세-321절"><strong>5.3 이해관계자 피드백의 통합</strong> (상세: 3.2.1절)</h3>
<h4 id="조기-피드백의-효과와-전체-연결">조기 피드백의 효과와 전체 연결</h4>
<pre><code>조기/빈번 피드백
    ↓
오해 방지 → 요구사항 품질 → 테스트 베이시스 개선 → 테스트 분석 용이
    ↓
변경 조기 반영 → 재작업 감소 → 비용 절감 (1.3 조기 테스팅 원리)
    ↓
이해도 향상 → 리스크 감소 (5.2) → 중요 기능 집중
    ↓
지속적 협업 → 전체 팀 접근법 (1.5.2) → 품질 책임 공유</code></pre><hr>
<h2 id="6-테스트-도구-지원-제6장">6. 테스트 도구 지원 (제6장)</h2>
<h3 id="61-테스트-도구-유형과-활동-매핑-상세-61절"><strong>6.1 테스트 도구 유형과 활동 매핑</strong> (상세: 6.1절)</h3>
<pre><code>도구 유형                → 지원 활동               → 관련 프로세스
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
테스트 관리 도구         → 계획/모니터링/제어       → 5.1, 5.3절
                        → 요구사항/결함 관리       → 5.5절, 추적성 (1.4.4)

정적 테스팅 도구         → 리뷰/정적 분석          → 3장
                        → CI 통합                → 2.1.4 DevOps

테스트 설계/구현 도구    → 테스트 케이스 생성       → 테스트 설계 (1.4.1)
                        → 테스트 데이터 생성       → 4장 테스트 기법

테스트 실행/커버리지    → 자동 실행               → 테스트 실행 (1.4.1)
                        → 커버리지 측정           → 4.3 화이트박스

비기능 테스팅 도구       → 성능/부하/보안 테스트    → 2.2.2 비기능 테스트

DevOps 도구             → CI/CD 파이프라인        → 2.1.4 DevOps
                        → 자동 빌드/배포          → 리그레션 (2.2.3)

협업 도구               → 커뮤니케이션            → 전체 팀 (1.5.2)
                        → 리뷰                   → 3.2절

형상 관리 도구           → 버전 관리/추적          → 5.4 형상 관리</code></pre><hr>
<h3 id="62-테스트-자동화의-전략적-가치-상세-62절"><strong>6.2 테스트 자동화의 전략적 가치</strong> (상세: 6.2절)</h3>
<h4 id="자동화의-효과와-프로세스-연계">자동화의 효과와 프로세스 연계</h4>
<pre><code>자동화 효과              → 실현 방법                → 관련 개념
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
반복 작업 감소          → 리그레션 자동화          → 2.2.3, 테스트 효과 원리
시간 절약              → 빠른 실행                → 피드백 속도 (2.1.4 DevOps)

일관성/재현성          → 표준화된 실행            → 품질 보증 (1.2.2)
                       → 테스트 데이터 체계 생성   → 테스트 기법 (4장) 적용

객관적 평가            → 커버리지 측정            → 4.3 화이트박스
                       → 메트릭 수집              → 5.3.1 테스트 메트릭

정보 접근성            → 자동 보고                → 5.3.2 테스트 보고서
                       → 대시보드                → 5.3.3 상황 전달

조기 결함 발견          → CI/CD 통합              → 2.1.5 시프트 레프트
                       → 빠른 피드백              → 2.1.4 DevOps</code></pre><h4 id="자동화-리스크와-완화">자동화 리스크와 완화</h4>
<pre><code>리스크                  → 완화 방안                → 관련 개념
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
비현실적 기대           → 적절한 계획              → 5.1 테스트 계획
비용/노력 과소평가      → 정확한 추정              → 5.1.4 추정 기법

부적절한 사용           → 자동화 전략 수립          → 테스트 피라미드 (5.1.6)
                       → 수동 vs 자동 구분        → 3사분면 탐색적 (5.1.7)

과도한 의존             → 비판적 사고 유지          → 1.5.1 필수 기술
                       → 탐색적 테스팅 병행        → 4.4.2

도구 종속성             → 표준 도구 선택            → 평가 기준
유지보수 부담           → 테스트 설계 품질          → 4장 테스트 기법
                       → 형상 관리                → 5.4절</code></pre><hr>
<h2 id="7-통합-워크플로우-전체-연결도">7. 통합 워크플로우: 전체 연결도</h2>
<h3 id="71-프로젝트-시작부터-완료까지의-통합-흐름"><strong>7.1 프로젝트 시작부터 완료까지의 통합 흐름</strong></h3>
<pre><code>[프로젝트 시작]
     ↓
1. 테스트 계획 (5.1)
   ├→ SDLC 선택 (2.1) 영향 반영
   ├→ 리스크 분석 (5.2) 수행
   ├→ 테스트 전략 수립
   │  ├→ 테스트 레벨 (2.2.1) 정의
   │  ├→ 테스트 유형 (2.2.2) 선택
   │  ├→ 테스트 피라미드 (5.1.6) 적용
   │  └→ 테스팅 사분면 (5.1.7) 고려
   ├→ 테스트 노력 추정 (5.1.4)
   ├→ 시작/완료 조건 (5.1.3) 정의
   └→ 도구 선택 (6.1)
     ↓
2. 정적 테스팅 (3장) - 시프트 레프트 (2.1.5)
   ├→ 요구사항 리뷰 (3.2)
   ├→ 설계 리뷰
   ├→ 정적 분석
   └→ 조기 피드백 (3.2.1)
     ↓
3. 테스트 분석 (1.4.1)
   ├→ 테스트 베이시스 분석
   ├→ 테스트 컨디션 정의
   ├→ 리스크 기반 우선순위 (5.2)
   └→ 추적성 구축 (1.4.4)
     ↓
4. 테스트 설계 (1.4.1)
   ├→ 테스트 기법 적용 (4장)
   │  ├→ 블랙박스 (4.2)
   │  ├→ 화이트박스 (4.3)
   │  ├→ 경험 기반 (4.4)
   │  └→ 협업 기반 (4.5)
   ├→ 테스트 케이스 작성
   └→ 테스트 환경 설계
     ↓
5. 테스트 구현 (1.4.1)
   ├→ 우선순위지정 (5.1.5)
   ├→ 테스트 자동화 (6.2)
   ├→ 테스트 데이터 준비
   └→ 환경 구축
     ↓
6. 테스트 실행 (1.4.1)
   ├→ 테스트 레벨별 실행 (2.2.1)
   ├→ 결함 발견 → 결함 관리 (5.5)
   ├→ 확인 테스팅 (2.2.3)
   ├→ 리그레션 테스팅 (2.2.3)
   └→ 모니터링/제어 (5.3)
        ↓
7. 테스트 완료 (1.4.1)
   ├→ 테스트 완료 보고서 (5.3.2)
   ├→ 결함 분석
   ├→ 메트릭 수집 (5.3.1)
   └→ 회고 (2.1.6)
        ↓
[프로세스 개선 → 다음 프로젝트 반영]</code></pre><hr>
<h3 id="72-핵심-연결-관계-요약"><strong>7.2 핵심 연결 관계 요약</strong></h3>
<h4 id="a-조기-테스팅-체인">A. 조기 테스팅 체인</h4>
<pre><code>조기 테스팅 원리 (1.3)
    ↓
시프트 레프트 (2.1.5)
    ↓
정적 테스팅 (3장) + 리뷰 (3.2)
    ↓
TDD/ATDD/BDD (2.1.3)
    ↓
조기 피드백 (3.2.1)
    ↓
비용 절감 + 결함 예방</code></pre><h4 id="b-리스크-기반-테스팅-체인">B. 리스크 기반 테스팅 체인</h4>
<pre><code>리스크 원리 (1.3 - 결함 집중)
    ↓
리스크 관리 (5.2)
    ├→ 리스크 분석 (5.2.3)
    └→ 리스크 제어 (5.2.4)
    ↓
테스트 우선순위지정 (5.1.5)
    ↓
테스트 노력 집중
    ↓
테스트 메트릭 (5.3.1) - 리스크 추적</code></pre><h4 id="c-자동화와-효율성-체인">C. 자동화와 효율성 체인</h4>
<pre><code>테스트 효과 감소 원리 (1.3)
    ↓
리그레션 테스팅 (2.2.3)
    ↓
테스트 자동화 필요성 (6.2)
    ↓
DevOps/CI/CD (2.1.4)
    ↓
테스트 피라미드 (5.1.6)
    ↓
빠른 피드백 + 안정성</code></pre><h4 id="d-품질-보증-체인">D. 품질 보증 체인</h4>
<pre><code>품질 (1.2.2)
    ├→ 테스팅 (제품 지향, 교정)
    │   └→ 결함 발견/수정
    └→ 품질 보증 (프로세스 지향, 예방)
        ├→ 정적 테스팅 (3장)
        ├→ 리뷰 (3.2)
        └→ 회고/개선 (2.1.6)</code></pre><h4 id="e-협업과-의사소통-체인">E. 협업과 의사소통 체인</h4>
<pre><code>전체 팀 접근법 (1.5.2)
    ↓
협업 기반 접근법 (4.5)
    ├→ 사용자 스토리 작성 (4.5.1)
    ├→ 인수 조건 (4.5.2)
    └→ ATDD (4.5.3)
    ↓
리뷰 참여 (3.2)
    ↓
조기 피드백 (3.2.1)
    ↓
의사소통 기술 (1.5.1)
    ↓
테스트 보고 (5.3.2)</code></pre><hr>
<h2 id="8-실무-적용-시나리오별-참조-가이드">8. 실무 적용 시나리오별 참조 가이드</h2>
<h3 id="시나리오-1-새-프로젝트-테스트-계획-수립"><strong>시나리오 1: 새 프로젝트 테스트 계획 수립</strong></h3>
<pre><code>1단계: 정황 분석
   → 1.4.2 정황 요소 확인
   → 2.1.1 SDLC 영향 분석

2단계: 전략 수립
   → 5.1.1 테스트 계획서 작성
   → 2.1.2 좋은 프랙티스 적용
   → 5.1.6 테스트 피라미드
   → 5.1.7 테스팅 사분면

3단계: 리스크 관리
   → 5.2 리스크 관리 전체
   → 5.2.3 제품 리스크 분석

4단계: 자원 계획
   → 5.1.4 추정 기법
   → 1.4.5 역할 배정
   → 6.1 도구 선택

5단계: 시작/완료 조건
   → 5.1.3 정의</code></pre><hr>
<h3 id="시나리오-2-애자일-프로젝트-테스팅"><strong>시나리오 2: 애자일 프로젝트 테스팅</strong></h3>
<pre><code>스프린트 계획
   → 5.1.2 반복 주기 기여
   → 4.5 협업 기반 접근법
   → 4.5.1 사용자 스토리 작성
   → 4.5.2 인수 조건 정의

스프린트 실행
   → 2.1.3 TDD/ATDD/BDD
   → 2.1.4 DevOps CI/CD
   → 2.2.3 지속적 리그레션
   → 6.2 테스트 자동화

스프린트 종료
   → 5.3.2 테스트 완료 보고
   → 2.1.6 회고
   → 1.5.2 전체 팀 협업</code></pre><hr>
<h3 id="시나리오-3-레거시-시스템-개선"><strong>시나리오 3: 레거시 시스템 개선</strong></h3>
<pre><code>현황 분석
   → 2.3 유지보수 테스팅
   → 3.2 기존 코드 리뷰
   → 5.2 리스크 분석

개선 전략
   → 2.1.5 시프트 레프트 도입
   → 6.2 자동화 도입 계획
   → 4.3 화이트박스로 커버리지 확인

실행
   → 2.2.3 리그레션 강화
   → 5.4 형상 관리 개선
   → 4.4.2 탐색적 테스팅 병행</code></pre><hr>
<h3 id="시나리오-4-고위험-시스템-테스팅"><strong>시나리오 4: 고위험 시스템 테스팅</strong></h3>
<pre><code>안전 분석
   → 5.2.2 제품 리스크 중점
   → 1.5.3 높은 독립성 확보
   → 2.2.1 모든 테스트 레벨 수행

철저한 테스팅
   → 4.2.4 상태 전이 - 모든 전이
   → 4.3 화이트박스 - 분기 커버리지
   → 3.2.4 인스펙션 (공식 리뷰)

문서화
   → 5.5 결함 관리 엄격
   → 5.4 형상 관리 철저
   → 5.3.2 상세 보고서</code></pre><hr>
<h2 id="9-주요-표준-및-모델-참조">9. 주요 표준 및 모델 참조</h2>
<h3 id="isoiecieee-표준"><strong>ISO/IEC/IEEE 표준</strong></h3>
<ul>
<li><strong>29119-1</strong>: 테스팅 개념 (1.1)</li>
<li><strong>29119-2</strong>: 테스트 프로세스 (1.4)</li>
<li><strong>29119-3</strong>: 테스트 계획/보고서 (5.1.1, 5.3.2, 5.5)</li>
<li><strong>29119-4</strong>: 테스트 기법 (4장)</li>
<li><strong>20246</strong>: 리뷰 프로세스 (3.2)</li>
<li><strong>25010</strong>: 품질 모델 (2.2.2, 5.2.2)</li>
<li><strong>31000</strong>: 리스크 관리 (5.2)</li>
<li><strong>14764</strong>: 유지보수 (2.3)</li>
</ul>
<h3 id="핵심-모델"><strong>핵심 모델</strong></h3>
<ul>
<li><strong>테스트 피라미드</strong> (5.1.6) → 테스트 레벨 배분</li>
<li><strong>테스팅 사분면</strong> (5.1.7) → 애자일 테스트 전략</li>
<li><strong>V-모델</strong> (2.1) → 테스트 레벨과 개발 단계 매핑</li>
</ul>
<hr>
<p><strong>이 통합 문서는 테스팅 개념들의 관계를 중심으로 재구성되었습니다. 각 항목의 세부 내용과 예제는 명시된 장/절을 반드시 참조하시기 바랍니다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS 컨퍼런스 [Games On AWS]]]></title>
            <link>https://velog.io/@ch_kang/AWS-%EC%BB%A8%ED%8D%BC%EB%9F%B0%EC%8A%A4-Games-On-AWS</link>
            <guid>https://velog.io/@ch_kang/AWS-%EC%BB%A8%ED%8D%BC%EB%9F%B0%EC%8A%A4-Games-On-AWS</guid>
            <pubDate>Tue, 14 Oct 2025 23:35:09 GMT</pubDate>
            <description><![CDATA[<h2 id="1-인조이-크래프톤">1. 인조이 [크래프톤]</h2>
<h3 id="1-3d-프린터-인게임-모델-구현">1) 3D 프린터 인게임 모델 구현</h3>
<p>크래프톤의 인조이는 AI 기반 3D 프린터 기술을 통해 2D 이미지를 3D 오브젝트로 자동 변환하는 혁신적인 기능을 제공합니다. 이 기능은 외부 사진을 게임 내에 업로드하면 AI가 해당 물체를 인식하여 3D 모델로 만들어주는 방식으로 작동합니다.</p>
<p>개발진은 3D 프린터로 만든 오브젝트를 게임 내에서 사용 가능한 실제 &#39;가구&#39;로 활용하거나 액세서리로 만들어 캐릭터에게 장착시킬 수 있다고 밝혔습니다.</p>
<p><strong>관련 자료:</strong></p>
<ul>
<li><a href="https://www.inven.co.kr/webzine/news/?news=303592">인조이 3D 프린터 기능 소개 - 인벤</a></li>
<li><a href="https://www.bloter.net/news/articleView.html?idxno=645472">크래프톤 인조이 AWS AI 활용 사례 - 블로터</a></li>
<li><a href="https://namu.news/article/2412312">게임스컴 2024 인조이 시연 - 연합뉴스</a></li>
</ul>
<h3 id="2-ai-이미지-생성-인게임-모델-구현">2) AI 이미지 생성 인게임 모델 구현</h3>
<p>인조이는 &#39;텍스트 투 이미지&#39; 기능을 통해 설명문을 입력하면 해당 이미지를 생성하여 캐릭터에 적용할 수 있습니다. 크래프톤은 AI가 생성한 벽지나 의상 패턴의 저작권 문제를 막기 위해 자체 필터링 및 후처리 기술을 개발했습니다.</p>
<p>게임에는 &#39;비디오 투 모션(Video to Motion)&#39;과 &#39;텍스트 투 이미지(Text to Image)&#39; 등 AI를 활용한 다양한 창작 도구가 제공됩니다.</p>
<p><strong>관련 자료:</strong></p>
<ul>
<li><a href="https://www.etnews.com/20250319000304">인조이 AI 창작 도구 - 전자신문</a></li>
<li><a href="https://m.news.nate.com/view/20250319n26311">인조이 쇼케이스 - 네이트뉴스</a></li>
</ul>
<h3 id="3-smart-npc">3) Smart NPC</h3>
<p>인조이의 &#39;스마트조이&#39; 기술은 플레이어가 플레이하지 않는 캐릭터들에게 각자의 직업, 성격, 서사를 부여해 스스로 생활하고 움직이게 하는 CPC(Co-Playable Character) 기술입니다. 엔비디아의 SLM(소형언어모델)을 적용한 NPC들이 등장해 실제로 사고하는 사람과 차별이 없는 언어활동을 구사하며 플레이어들과 상호작용합니다.</p>
<p><strong>관련 자료:</strong></p>
<ul>
<li><a href="https://news.mtn.co.kr/news-detail/2025010708585521513">크래프톤 CPC 기술 - MTN뉴스</a></li>
<li><a href="https://www.krafton.com/more-experience/ai/">크래프톤 AI 기술 소개</a></li>
</ul>
<hr>
<h2 id="2-배틀그라운드-파트너-ai-크래프톤">2. 배틀그라운드 파트너 AI [크래프톤]</h2>
<h3 id="ai와-듀오를-돌리는-상상">AI와 듀오를 돌리는 상상</h3>
<p>크래프톤은 CES 2025에서 배틀그라운드의 AI 동료 &#39;PUBG 얼라이(Ally)&#39;를 공개했습니다. 이 AI 캐릭터는 플레이어의 음성을 인식해 필요한 아이템이나 차량을 찾아오고, 적을 발견하면 경고도 합니다.</p>
<p>CPC(Co-Playable Character)라는 이 NPC는 플레이어와 경기 상황에 대해 실시간으로 대화를 나누며, 플레이어의 스타일에 맞춰 전략과 게임 플레이를 조정할 수 있습니다.</p>
<p>크래프톤은 엔비디아의 AI 기술력을 활용해 배틀그라운드에 사람보다 더 영리하고 정교한 비인칭 캐릭터(NPC)를 등장시키고 있으며, 이 캐릭터들이 실제 사람이 구동하는 캐릭터인지 AI로 구현된 NPC인지 구별할 수 없는 상황에서 함께 게임을 즐기게 됩니다.</p>
<p><strong>관련 자료:</strong></p>
<ul>
<li><a href="https://www.hankookilbo.com/News/Read/A2025010715180003913">배틀그라운드 AI 파트너 발표 - 한국일보</a></li>
<li><a href="https://news.mtn.co.kr/news-detail/2025010708585521513">크래프톤 엔비디아 협력 - MTN뉴스</a></li>
<li><a href="https://www.digitaltoday.co.kr/news/articleView.html?idxno=548963">배틀그라운드 CPC 기술 - 디지털투데이</a></li>
</ul>
<hr>
<h2 id="3-ai--tdd-게임듀오">3. AI + TDD [게임듀오]</h2>
<p><strong>참고:</strong> &quot;게임듀오&quot;라는 회사의 AI + TDD 구체적인 사례는 검색 결과에서 찾을 수 없었습니다. GameDuo라는 회사는 Archer Forest 게임을 개발한 회사로 확인되었으나, TDD 관련 사례는 공개되지 않은 것으로 보입니다.</p>
<p>대신 관련된 일반적인 AI + TDD 연구 사례를 공유드립니다:</p>
<p>최근 연구에 따르면 생성 AI(GenAI)를 TDD에 활용할 경우 개발자가 테스트를 작성하고 AI가 코드 생성을 감독하는 협업 패턴이나, AI가 전체 프로세스를 자동화하고 개발자가 최종 검증만 하는 완전 자동화 패턴으로 적용할 수 있습니다.</p>
<p><strong>관련 자료:</strong></p>
<ul>
<li><a href="https://arxiv.org/html/2405.10849v1">AI를 활용한 TDD 자동화 연구</a></li>
<li><a href="https://www.mechanical-orchard.com/insights/can-two-ais-play-the-tdd-pairing-game">AI 페어 프로그래밍과 TDD</a></li>
</ul>
<p>—</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity NavMesh AI 네비게이션 시스템 완벽 가이드]]></title>
            <link>https://velog.io/@ch_kang/Unity-NavMesh-AI-%EB%84%A4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@ch_kang/Unity-NavMesh-AI-%EB%84%A4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Fri, 08 Aug 2025 05:54:03 GMT</pubDate>
            <description><![CDATA[<h1 id="unity-navmesh-ai-네비게이션-시스템-완벽-가이드">Unity NavMesh AI 네비게이션 시스템 완벽 가이드</h1>
<p>Unity의 NavMesh(Navigation Mesh) 시스템은 AI 캐릭터들이 지형을 자동으로 탐색하고 장애물을 피해 목적지까지 이동할 수 있게 해주는 강력한 도구입니다. 이번 글에서는 NavMesh 시스템의 기본 설정부터 실제 AI 구현까지 단계별로 알아보겠습니다.</p>
<h2 id="navmesh란">NavMesh란?</h2>
<p>NavMesh는 <strong>Navigation Mesh</strong>의 줄임말로, AI 에이전트가 이동할 수 있는 영역을 미리 계산해둔 메쉬입니다. 이 시스템을 통해 AI는 복잡한 지형에서도 최적의 경로를 찾아 자동으로 이동할 수 있습니다.</p>
<h3 id="주요-특징">주요 특징</h3>
<ul>
<li><strong>자동 경로 찾기</strong>: A* 알고리즘 기반의 효율적인 길찾기</li>
<li><strong>장애물 회피</strong>: 동적 및 정적 장애물 자동 회피</li>
<li><strong>점프 및 낙하</strong>: 플랫폼 간 이동 지원</li>
<li><strong>성능 최적화</strong>: 미리 계산된 메쉬로 실시간 성능 향상</li>
</ul>
<h2 id="1-ai-navigation-패키지-설치">1. AI Navigation 패키지 설치</h2>
<p>먼저 Unity에서 AI Navigation 패키지를 설치해야 합니다.</p>
<h3 id="설치-단계">설치 단계</h3>
<ol>
<li><strong>Window</strong> → <strong>Package Manager</strong> 열기</li>
<li><strong>Unity Registry</strong> 선택</li>
<li>검색창에 <strong>&quot;nav&quot;</strong> 입력</li>
<li><strong>AI Navigation</strong> 패키지 찾기</li>
<li><strong>Install</strong> 버튼 클릭</li>
</ol>
<pre><code>Package Manager → Unity Registry → &quot;nav&quot; 검색 → AI Navigation → Install</code></pre><h2 id="2-navmesh-surface-설정">2. NavMesh Surface 설정</h2>
<h3 id="2-1-기본-지면-설정">2-1. 기본 지면 설정</h3>
<p>NavMesh 시스템의 첫 번째 단계는 AI가 걸을 수 있는 표면을 정의하는 것입니다.</p>
<pre><code class="language-csharp">// 기본 지면 오브젝트(Plane) 선택
// Add Component → NavMesh Surface</code></pre>
<h3 id="2-2-navmesh-베이킹">2-2. NavMesh 베이킹</h3>
<ol>
<li><strong>Plane 오브젝트 선택</strong></li>
<li><strong>Add Component</strong> → <strong>NavMesh Surface</strong></li>
<li><strong>Bake</strong> 버튼 클릭</li>
</ol>
<p>베이킹 후 파란색 영역이 표시되지 않으면:</p>
<ul>
<li><strong>Scene 뷰</strong> 상단의 <strong>기즈모 버튼</strong> 클릭</li>
<li><strong>NavMesh</strong> 체크박스 활성화</li>
</ul>
<h3 id="2-3-걸을-수-있는-영역-확장">2-3. 걸을 수 있는 영역 확장</h3>
<p>기본적으로는 Plane만 걸을 수 있는 영역으로 설정됩니다. 다른 오브젝트들도 포함시키려면:</p>
<ol>
<li><strong>Hierarchy</strong>에서 <strong>Shift</strong> 키를 누른 채로 <strong>Ground</strong>와 <strong>Obstacles</strong> 선택</li>
<li><strong>Inspector</strong>에서 <strong>Navigation</strong> 탭으로 이동</li>
<li><strong>Navigation Static</strong> 체크박스 활성화</li>
<li><strong>NavMesh Surface</strong>에서 다시 <strong>Bake</strong> 클릭</li>
</ol>
<h2 id="3-navmesh-agent-설정-최적화">3. NavMesh Agent 설정 최적화</h2>
<h3 id="3-1-기본-에이전트-설정">3-1. 기본 에이전트 설정</h3>
<p><strong>Window</strong> → <strong>AI</strong> → <strong>Navigation</strong>에서 에이전트 설정을 조정할 수 있습니다.</p>
<pre><code>Navigation Window 설정:
- Agent Radius: 0.25 (에이전트의 반지름)
- Agent Height: 0.7 (에이전트의 키)
- Max Slope: 35 (오를 수 있는 최대 경사도)
- Step Height: 0.1 (오를 수 있는 계단 높이)</code></pre><h3 id="3-2-최적화된-설정-예시">3-2. 최적화된 설정 예시</h3>
<pre><code class="language-csharp">// 권장 설정값
Agent Radius: 0.25f     // 좁은 공간 통과 가능
Agent Height: 0.7f      // 일반적인 캐릭터 크기
Max Slope: 35f          // 적당한 경사면 이동
Step Height: 0.1f       // 작은 턱 극복 가능</code></pre>
<h2 id="4-기본-ai-스크립트-구현">4. 기본 AI 스크립트 구현</h2>
<h3 id="4-1-간단한-navmesh-agent-스크립트">4-1. 간단한 NavMesh Agent 스크립트</h3>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.AI;

public class SimpleAI : MonoBehaviour
{
    [Header(&quot;Navigation&quot;)]
    public NavMeshAgent agent;
    public Transform target;

    private void Start()
    {
        // NavMeshAgent 컴포넌트 자동 할당
        if (agent == null)
            agent = GetComponent&lt;NavMeshAgent&gt;();
    }

    private void Update()
    {
        // 타겟이 설정되어 있으면 해당 위치로 이동
        if (target != null)
        {
            agent.SetDestination(target.position);
        }
    }
}</code></pre>
<h3 id="4-2-개선된-ai-스크립트">4-2. 개선된 AI 스크립트</h3>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.AI;

public class AdvancedAI : MonoBehaviour
{
    [Header(&quot;Navigation&quot;)]
    public NavMeshAgent agent;
    public Transform target;

    [Header(&quot;AI Settings&quot;)]
    public float detectionRange = 10f;
    public float stopDistance = 2f;

    private void Start()
    {
        agent = GetComponent&lt;NavMeshAgent&gt;();

        // 에이전트 설정
        agent.stoppingDistance = stopDistance;
    }

    private void Update()
    {
        if (target != null)
        {
            float distanceToTarget = Vector3.Distance(transform.position, target.position);

            // 탐지 범위 내에 있을 때만 추적
            if (distanceToTarget &lt;= detectionRange)
            {
                agent.SetDestination(target.position);
            }
            else
            {
                // 범위를 벗어나면 정지
                agent.ResetPath();
            }
        }
    }

    private void OnDrawGizmosSelected()
    {
        // 탐지 범위 시각화
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, detectionRange);

        // 정지 거리 시각화
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, stopDistance);
    }
}</code></pre>
<h2 id="5-점프-및-낙하-설정">5. 점프 및 낙하 설정</h2>
<h3 id="5-1-off-mesh-link를-통한-점프-구현">5-1. Off Mesh Link를 통한 점프 구현</h3>
<p>플랫폼 간 점프나 낙하가 필요한 경우 <strong>Off Mesh Link</strong>를 사용합니다.</p>
<h4 id="자동-링크-생성">자동 링크 생성</h4>
<ol>
<li><strong>NavMesh Surface</strong> 선택</li>
<li><strong>Advanced</strong> 섹션 펼치기</li>
<li><strong>Generate Links</strong> 체크박스 활성화</li>
<li><strong>Jump Distance</strong>: 1.0 설정 (점프 가능 거리)</li>
<li><strong>Drop Height</strong>: 3.0 설정 (낙하 가능 높이)</li>
<li><strong>Bake</strong> 다시 실행</li>
</ol>
<h4 id="수동-링크-생성">수동 링크 생성</h4>
<pre><code class="language-csharp">// Off Mesh Link 컴포넌트를 수동으로 추가
// Start Point와 End Point를 설정하여 정확한 점프 지점 지정</code></pre>
<h3 id="5-2-점프-애니메이션-연동">5-2. 점프 애니메이션 연동</h3>
<pre><code class="language-csharp">public class AIAnimationController : MonoBehaviour
{
    private NavMeshAgent agent;
    private Animator animator;

    private void Start()
    {
        agent = GetComponent&lt;NavMeshAgent&gt;();
        animator = GetComponent&lt;Animator&gt;();
    }

    private void Update()
    {
        // 이동 속도에 따른 애니메이션
        float speed = agent.velocity.magnitude;
        animator.SetFloat(&quot;Speed&quot;, speed);

        // Off Mesh Link 사용 중인지 확인 (점프/낙하)
        if (agent.isOnOffMeshLink)
        {
            animator.SetTrigger(&quot;Jump&quot;);
        }
    }
}</code></pre>
<h2 id="6-다중-ai-에이전트-관리">6. 다중 AI 에이전트 관리</h2>
<h3 id="6-1-여러-ai-동시-제어">6-1. 여러 AI 동시 제어</h3>
<pre><code class="language-csharp">using System.Collections.Generic;
using UnityEngine;

public class AIManager : MonoBehaviour
{
    [Header(&quot;AI Management&quot;)]
    public List&lt;GameObject&gt; aiPrefabs;
    public Transform[] spawnPoints;
    public Transform commonTarget;

    [Header(&quot;Spawn Settings&quot;)]
    public int aiCount = 10;

    private List&lt;GameObject&gt; spawnedAIs = new List&lt;GameObject&gt;();

    private void Start()
    {
        SpawnMultipleAIs();
    }

    private void SpawnMultipleAIs()
    {
        for (int i = 0; i &lt; aiCount; i++)
        {
            // 랜덤 스폰 지점 선택
            Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];

            // 랜덤 AI 프리팹 선택
            GameObject aiPrefab = aiPrefabs[Random.Range(0, aiPrefabs.Length)];

            // AI 생성
            GameObject newAI = Instantiate(aiPrefab, spawnPoint.position, spawnPoint.rotation);

            // 타겟 설정
            SimpleAI aiScript = newAI.GetComponent&lt;SimpleAI&gt;();
            if (aiScript != null)
            {
                aiScript.target = commonTarget;
            }

            spawnedAIs.Add(newAI);
        }
    }

    public void ChangeAllTargets(Transform newTarget)
    {
        foreach (GameObject ai in spawnedAIs)
        {
            SimpleAI aiScript = ai.GetComponent&lt;SimpleAI&gt;();
            if (aiScript != null)
            {
                aiScript.target = newTarget;
            }
        }
    }
}</code></pre>
<h2 id="7-성능-최적화-팁">7. 성능 최적화 팁</h2>
<h3 id="7-1-navmesh-agent-최적화">7-1. NavMesh Agent 최적화</h3>
<pre><code class="language-csharp">public class OptimizedAI : MonoBehaviour
{
    private NavMeshAgent agent;
    private float updateInterval = 0.1f; // 0.1초마다 업데이트
    private float lastUpdateTime;

    private void Start()
    {
        agent = GetComponent&lt;NavMeshAgent&gt;();

        // 성능 최적화 설정
        agent.acceleration = 8f;        // 가속도
        agent.angularSpeed = 120f;      // 회전 속도
        agent.obstacleAvoidanceType = ObstacleAvoidanceType.LowQualityObstacleAvoidance;
    }

    private void Update()
    {
        // 일정 간격으로만 경로 업데이트
        if (Time.time - lastUpdateTime &gt;= updateInterval)
        {
            UpdateNavigation();
            lastUpdateTime = Time.time;
        }
    }

    private void UpdateNavigation()
    {
        // 실제 네비게이션 로직
        if (target != null &amp;&amp; agent.enabled)
        {
            agent.SetDestination(target.position);
        }
    }
}</code></pre>
<h3 id="7-2-거리-기반-lod-시스템">7-2. 거리 기반 LOD 시스템</h3>
<pre><code class="language-csharp">public class AILODManager : MonoBehaviour
{
    [Header(&quot;LOD Settings&quot;)]
    public float highDetailDistance = 20f;
    public float mediumDetailDistance = 50f;

    private Transform player;
    private NavMeshAgent agent;

    private void Start()
    {
        player = FindObjectOfType&lt;Player&gt;().transform;
        agent = GetComponent&lt;NavMeshAgent&gt;();
    }

    private void Update()
    {
        float distanceToPlayer = Vector3.Distance(transform.position, player.position);

        // 거리에 따른 업데이트 빈도 조절
        if (distanceToPlayer &lt;= highDetailDistance)
        {
            // 고품질: 매 프레임 업데이트
            agent.enabled = true;
            GetComponent&lt;Animator&gt;().enabled = true;
        }
        else if (distanceToPlayer &lt;= mediumDetailDistance)
        {
            // 중품질: 낮은 빈도 업데이트
            agent.enabled = true;
            GetComponent&lt;Animator&gt;().enabled = false;
        }
        else
        {
            // 저품질: 네비게이션 비활성화
            agent.enabled = false;
            GetComponent&lt;Animator&gt;().enabled = false;
        }
    }
}</code></pre>
<h2 id="8-고급-기능-구현">8. 고급 기능 구현</h2>
<h3 id="8-1-순찰-시스템">8-1. 순찰 시스템</h3>
<pre><code class="language-csharp">using System.Collections;
using UnityEngine;

public class PatrolAI : MonoBehaviour
{
    [Header(&quot;Patrol Settings&quot;)]
    public Transform[] patrolPoints;
    public float waitTime = 2f;
    public bool randomPatrol = false;

    private NavMeshAgent agent;
    private int currentPatrolIndex = 0;
    private bool isWaiting = false;

    private void Start()
    {
        agent = GetComponent&lt;NavMeshAgent&gt;();

        if (patrolPoints.Length &gt; 0)
        {
            agent.SetDestination(patrolPoints[0].position);
        }
    }

    private void Update()
    {
        // 목적지에 도달했는지 확인
        if (!agent.pathPending &amp;&amp; agent.remainingDistance &lt; 0.5f &amp;&amp; !isWaiting)
        {
            StartCoroutine(WaitAndMoveToNext());
        }
    }

    private IEnumerator WaitAndMoveToNext()
    {
        isWaiting = true;
        yield return new WaitForSeconds(waitTime);

        // 다음 순찰 지점 결정
        if (randomPatrol)
        {
            currentPatrolIndex = Random.Range(0, patrolPoints.Length);
        }
        else
        {
            currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
        }

        // 다음 목적지로 이동
        agent.SetDestination(patrolPoints[currentPatrolIndex].position);
        isWaiting = false;
    }
}</code></pre>
<h3 id="8-2-상태-기반-ai">8-2. 상태 기반 AI</h3>
<pre><code class="language-csharp">using UnityEngine;

public enum AIState
{
    Idle,
    Patrol,
    Chase,
    Attack,
    Return
}

public class StateMachineAI : MonoBehaviour
{
    [Header(&quot;AI Settings&quot;)]
    public Transform player;
    public float detectionRange = 10f;
    public float attackRange = 2f;
    public float loseTargetRange = 15f;

    [Header(&quot;Patrol&quot;)]
    public Transform[] patrolPoints;

    private NavMeshAgent agent;
    private AIState currentState = AIState.Patrol;
    private Vector3 initialPosition;
    private int patrolIndex = 0;

    private void Start()
    {
        agent = GetComponent&lt;NavMeshAgent&gt;();
        initialPosition = transform.position;

        if (patrolPoints.Length &gt; 0)
        {
            agent.SetDestination(patrolPoints[0].position);
        }
    }

    private void Update()
    {
        float distanceToPlayer = Vector3.Distance(transform.position, player.position);

        switch (currentState)
        {
            case AIState.Patrol:
                HandlePatrolState(distanceToPlayer);
                break;

            case AIState.Chase:
                HandleChaseState(distanceToPlayer);
                break;

            case AIState.Attack:
                HandleAttackState(distanceToPlayer);
                break;

            case AIState.Return:
                HandleReturnState();
                break;
        }
    }

    private void HandlePatrolState(float distanceToPlayer)
    {
        // 플레이어 탐지
        if (distanceToPlayer &lt;= detectionRange)
        {
            ChangeState(AIState.Chase);
            return;
        }

        // 순찰 지점 도달 확인
        if (!agent.pathPending &amp;&amp; agent.remainingDistance &lt; 0.5f)
        {
            patrolIndex = (patrolIndex + 1) % patrolPoints.Length;
            agent.SetDestination(patrolPoints[patrolIndex].position);
        }
    }

    private void HandleChaseState(float distanceToPlayer)
    {
        // 공격 범위 내 진입
        if (distanceToPlayer &lt;= attackRange)
        {
            ChangeState(AIState.Attack);
            return;
        }

        // 플레이어를 놓침
        if (distanceToPlayer &gt; loseTargetRange)
        {
            ChangeState(AIState.Return);
            return;
        }

        // 플레이어 추적
        agent.SetDestination(player.position);
    }

    private void HandleAttackState(float distanceToPlayer)
    {
        // 공격 범위를 벗어남
        if (distanceToPlayer &gt; attackRange)
        {
            ChangeState(AIState.Chase);
            return;
        }

        // 공격 로직 (애니메이션, 데미지 등)
        // 여기서 실제 공격 구현
    }

    private void HandleReturnState()
    {
        // 초기 위치로 복귀
        agent.SetDestination(initialPosition);

        if (Vector3.Distance(transform.position, initialPosition) &lt; 1f)
        {
            ChangeState(AIState.Patrol);
        }
    }

    private void ChangeState(AIState newState)
    {
        currentState = newState;
        Debug.Log($&quot;AI State changed to: {newState}&quot;);
    }
}</code></pre>
<h2 id="9-문제-해결-및-디버깅">9. 문제 해결 및 디버깅</h2>
<h3 id="9-1-일반적인-문제들">9-1. 일반적인 문제들</h3>
<h4 id="navmesh가-생성되지-않는-경우">NavMesh가 생성되지 않는 경우</h4>
<pre><code class="language-csharp">// 해결 방법:
// 1. 오브젝트가 Navigation Static으로 설정되어 있는지 확인
// 2. NavMesh Surface 컴포넌트가 올바르게 설정되어 있는지 확인
// 3. Bake 버튼을 다시 클릭</code></pre>
<h4 id="ai가-이동하지-않는-경우">AI가 이동하지 않는 경우</h4>
<pre><code class="language-csharp">public class NavMeshDebugger : MonoBehaviour
{
    private NavMeshAgent agent;

    private void Start()
    {
        agent = GetComponent&lt;NavMeshAgent&gt;();
    }

    private void Update()
    {
        // 디버그 정보 출력
        if (agent != null)
        {
            Debug.Log($&quot;Agent enabled: {agent.enabled}&quot;);
            Debug.Log($&quot;Has path: {agent.hasPath}&quot;);
            Debug.Log($&quot;Path status: {agent.pathStatus}&quot;);
            Debug.Log($&quot;Remaining distance: {agent.remainingDistance}&quot;);
        }
    }
}</code></pre>
<h3 id="9-2-성능-모니터링">9-2. 성능 모니터링</h3>
<pre><code class="language-csharp">using UnityEngine;

public class NavMeshProfiler : MonoBehaviour
{
    [Header(&quot;Performance Monitoring&quot;)]
    public bool showDebugInfo = true;

    private NavMeshAgent[] allAgents;

    private void Start()
    {
        allAgents = FindObjectsOfType&lt;NavMeshAgent&gt;();
    }

    private void OnGUI()
    {
        if (showDebugInfo)
        {
            GUILayout.BeginArea(new Rect(10, 10, 300, 200));
            GUILayout.Label($&quot;Active NavMesh Agents: {allAgents.Length}&quot;);

            int activeAgents = 0;
            foreach (var agent in allAgents)
            {
                if (agent.enabled &amp;&amp; agent.gameObject.activeInHierarchy)
                    activeAgents++;
            }

            GUILayout.Label($&quot;Enabled Agents: {activeAgents}&quot;);
            GUILayout.Label($&quot;FPS: {1f / Time.unscaledDeltaTime:F1}&quot;);
            GUILayout.EndArea();
        }
    }
}</code></pre>
<h2 id="결론">결론</h2>
<p>Unity의 NavMesh 시스템은 복잡한 AI 내비게이션을 간단하게 구현할 수 있게 해주는 강력한 도구입니다. 기본적인 설정부터 고급 상태 머신까지, 이 가이드를 통해 여러분만의 지능적인 AI 시스템을 구축할 수 있을 것입니다.</p>
<p><strong>핵심 포인트:</strong></p>
<ul>
<li><strong>올바른 NavMesh 베이킹</strong>이 모든 것의 기초</li>
<li><strong>성능 최적화</strong>를 통한 다중 AI 관리</li>
<li><strong>상태 기반 시스템</strong>으로 복잡한 AI 행동 구현</li>
<li><strong>디버깅 도구</strong>를 활용한 문제 해결</li>
</ul>
<p>이제 여러분의 게임에 생동감 넘치는 AI 캐릭터들을 추가해보세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity 오브젝트 풀링 시스템 완전 가이드]]></title>
            <link>https://velog.io/@ch_kang/Unity-%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8-%ED%92%80%EB%A7%81-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@ch_kang/Unity-%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8-%ED%92%80%EB%A7%81-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Wed, 06 Aug 2025 10:59:19 GMT</pubDate>
            <description><![CDATA[<h1 id="unity-오브젝트-풀링-시스템-완전-가이드">Unity 오브젝트 풀링 시스템 완전 가이드</h1>
<p>게임 개발에서 성능 최적화는 매우 중요한 요소입니다. 특히 총알, 적, 이펙트 등 많은 수의 오브젝트를 빈번하게 생성하고 파괴해야 하는 경우, 오브젝트 풀링(Object Pooling) 시스템은 필수적인 기술입니다. 이번 글에서는 Unity에서 효율적인 오브젝트 풀링 시스템을 구현하는 방법을 자세히 알아보겠습니다.</p>
<h2 id="오브젝트-풀링이란">오브젝트 풀링이란?</h2>
<p>오브젝트 풀링은 <strong>미리 정해진 수의 게임 오브젝트를 생성해두고 재사용하는 시스템</strong>입니다. 매번 <code>Instantiate()</code>와 <code>Destroy()</code>를 호출하는 대신, 이미 생성된 오브젝트를 활성화/비활성화하여 사용함으로써 성능을 크게 향상시킬 수 있습니다.</p>
<h3 id="장점">장점</h3>
<ul>
<li><strong>성능 향상</strong>: 가비지 컬렉션 빈도 감소</li>
<li><strong>메모리 효율성</strong>: 일정한 메모리 사용량 유지</li>
<li><strong>프레임 드롭 방지</strong>: 실시간 생성/파괴로 인한 끊김 현상 제거</li>
</ul>
<h2 id="시스템-구조-개요">시스템 구조 개요</h2>
<p>오브젝트 풀링 시스템은 두 개의 핵심 클래스로 구성됩니다:</p>
<ol>
<li><strong>ObjectPool</strong>: 풀링 시스템의 메인 매니저</li>
<li><strong>PooledObject</strong>: 각 풀링 오브젝트에 붙는 헬퍼 컴포넌트</li>
</ol>
<h2 id="dictionary와-key-value-pair의-이해">Dictionary와 Key-Value Pair의 이해</h2>
<p>풀링 시스템을 이해하기 전에, C#의 Dictionary 자료구조를 알아야 합니다.</p>
<h3 id="key-value-pair란">Key-Value Pair란?</h3>
<p><strong>Key-Value Pair(키-값 쌍)</strong>은 고유한 식별자(키)와 그에 연결된 특정 값으로 구성된 데이터 구조입니다.</p>
<p><strong>실생활 예시:</strong></p>
<ul>
<li>사전에서 &#39;단어(키)&#39;와 &#39;뜻(값)&#39;</li>
<li>전화번호부에서 &#39;이름(키)&#39;와 &#39;전화번호(값)&#39;</li>
</ul>
<h3 id="dictionary-in-c">Dictionary in C#</h3>
<pre><code class="language-csharp">Dictionary&lt;TKey, TValue&gt;</code></pre>
<p><strong>오브젝트 풀링에서의 적용:</strong></p>
<ul>
<li><strong>Key (TKey)</strong>: <code>GameObject</code> 프리팹 (총알, 적, 이펙트 등)</li>
<li><strong>Value (TValue)</strong>: <code>Queue&lt;GameObject&gt;</code> (해당 프리팹의 오브젝트들을 담는 큐)</li>
</ul>
<pre><code class="language-csharp">// 예시: 각 프리팹별로 오브젝트 큐를 관리
Dictionary&lt;GameObject, Queue&lt;GameObject&gt;&gt; poolDictionary;</code></pre>
<h2 id="objectpool-클래스-상세-분석">ObjectPool 클래스 상세 분석</h2>
<h3 id="핵심-변수들">핵심 변수들</h3>
<pre><code class="language-csharp">public class ObjectPool : MonoBehaviour
{
    // 싱글톤 패턴으로 전역 접근 가능
    public static ObjectPool instance;

    // 각 타입별 기본 풀 크기
    [SerializeField] private int poolSize = 10;

    // 핵심: 프리팹별 오브젝트 큐를 관리하는 딕셔너리
    private Dictionary&lt;GameObject, Queue&lt;GameObject&gt;&gt; poolDictionary;
}</code></pre>
<h3 id="주요-메서드들">주요 메서드들</h3>
<h4 id="1-awake---싱글톤-초기화">1. Awake() - 싱글톤 초기화</h4>
<pre><code class="language-csharp">private void Awake()
{
    // 싱글톤 패턴 구현
    if (instance == null)
    {
        instance = this;
        DontDestroyOnLoad(gameObject);
        poolDictionary = new Dictionary&lt;GameObject, Queue&lt;GameObject&gt;&gt;();
    }
    else
    {
        Destroy(gameObject);
    }
}</code></pre>
<h4 id="2-getobject---풀에서-오브젝트-가져오기">2. GetObject() - 풀에서 오브젝트 가져오기</h4>
<pre><code class="language-csharp">public GameObject GetObject(GameObject prefab)
{
    // 해당 프리팹의 풀이 존재하지 않으면 새로 생성
    if (!poolDictionary.ContainsKey(prefab))
    {
        InitializeNewPool(prefab);
    }

    // 풀이 비어있으면 새 오브젝트 생성
    if (poolDictionary[prefab].Count == 0)
    {
        CreateNewObject(prefab);
    }

    // 풀에서 오브젝트 가져와서 활성화 후 반환
    GameObject objectToSpawn = poolDictionary[prefab].Dequeue();
    objectToSpawn.SetActive(true);

    return objectToSpawn;
}</code></pre>
<h4 id="3-returnobject---오브젝트를-풀에-반환">3. ReturnObject() - 오브젝트를 풀에 반환</h4>
<pre><code class="language-csharp">public void ReturnObject(GameObject objectToReturn, float delay = 0.001f)
{
    StartCoroutine(DelayReturn(delay, objectToReturn));
}

private IEnumerator DelayReturn(float delay, GameObject objectToReturn)
{
    yield return new WaitForSeconds(delay);
    ReturnToPool(objectToReturn);
}</code></pre>
<h4 id="4-returntopool---실제-풀-반환-로직">4. ReturnToPool() - 실제 풀 반환 로직</h4>
<pre><code class="language-csharp">private void ReturnToPool(GameObject objectToReturn)
{
    // PooledObject 컴포넌트에서 원본 프리팹 정보 가져오기
    PooledObject pooledComponent = objectToReturn.GetComponent&lt;PooledObject&gt;();
    GameObject originalPrefab = pooledComponent.originalPrefab;

    // 오브젝트 비활성화 및 부모 설정
    objectToReturn.SetActive(false);
    objectToReturn.transform.parent = transform;

    // 해당 프리팹의 풀에 다시 추가
    poolDictionary[originalPrefab].Enqueue(objectToReturn);
}</code></pre>
<h4 id="5-initializenewpool---새-풀-초기화">5. InitializeNewPool() - 새 풀 초기화</h4>
<pre><code class="language-csharp">private void InitializeNewPool(GameObject prefab)
{
    // 새 프리팹을 위한 큐 생성
    poolDictionary[prefab] = new Queue&lt;GameObject&gt;();

    // 기본 크기만큼 오브젝트 미리 생성
    for (int i = 0; i &lt; poolSize; i++)
    {
        CreateNewObject(prefab);
    }
}</code></pre>
<h4 id="6-createnewobject---새-오브젝트-생성">6. CreateNewObject() - 새 오브젝트 생성</h4>
<pre><code class="language-csharp">private void CreateNewObject(GameObject prefab)
{
    // 새 오브젝트 생성
    GameObject newObject = Instantiate(prefab);

    // PooledObject 컴포넌트 추가 및 원본 프리팹 참조 설정
    PooledObject pooledComponent = newObject.AddComponent&lt;PooledObject&gt;();
    pooledComponent.originalPrefab = prefab;

    // 비활성화 후 풀에 추가
    newObject.SetActive(false);
    poolDictionary[prefab].Enqueue(newObject);
}</code></pre>
<h2 id="pooledobject-클래스">PooledObject 클래스</h2>
<pre><code class="language-csharp">public class PooledObject : MonoBehaviour
{
    // 원본 프리팹 참조를 저장하는 프로퍼티
    public GameObject originalPrefab { get; set; }
}</code></pre>
<p>이 간단한 헬퍼 클래스는 각 풀링된 오브젝트가 어떤 프리팹에서 생성되었는지 기억하여, 올바른 풀로 반환될 수 있도록 돕습니다.</p>
<h2 id="실제-사용-예시">실제 사용 예시</h2>
<h3 id="총알-발사-시스템">총알 발사 시스템</h3>
<pre><code class="language-csharp">public class Gun : MonoBehaviour
{
    [SerializeField] private GameObject bulletPrefab;
    [SerializeField] private Transform firePoint;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Fire();
        }
    }

    void Fire()
    {
        // 풀에서 총알 오브젝트 가져오기
        GameObject bullet = ObjectPool.instance.GetObject(bulletPrefab);
        bullet.transform.position = firePoint.position;
        bullet.transform.rotation = firePoint.rotation;

        // 3초 후 풀에 반환
        ObjectPool.instance.ReturnObject(bullet, 3f);
    }
}</code></pre>
<h3 id="적-스폰-시스템">적 스폰 시스템</h3>
<pre><code class="language-csharp">public class EnemySpawner : MonoBehaviour
{
    [SerializeField] private GameObject enemyPrefab;
    [SerializeField] private Transform[] spawnPoints;
    [SerializeField] private float spawnInterval = 2f;

    void Start()
    {
        InvokeRepeating(nameof(SpawnEnemy), 0f, spawnInterval);
    }

    void SpawnEnemy()
    {
        Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];

        GameObject enemy = ObjectPool.instance.GetObject(enemyPrefab);
        enemy.transform.position = spawnPoint.position;
        enemy.transform.rotation = spawnPoint.rotation;
    }
}</code></pre>
<h2 id="시스템-작동-플로우">시스템 작동 플로우</h2>
<h3 id="1-오브젝트-요청-과정">1. 오브젝트 요청 과정</h3>
<pre><code>GetObject(prefab) 호출
    ↓
프리팹의 풀이 존재하는가?
    ↓ (No)
InitializeNewPool() 실행
    ↓
풀에 사용 가능한 오브젝트가 있는가?
    ↓ (No)
CreateNewObject() 실행
    ↓
Dequeue()로 오브젝트 가져오기
    ↓
SetActive(true) 후 반환</code></pre><h3 id="2-오브젝트-반환-과정">2. 오브젝트 반환 과정</h3>
<pre><code>ReturnObject() 호출
    ↓
DelayReturn() 코루틴 시작
    ↓
지정된 시간 후 ReturnToPool() 실행
    ↓
PooledObject에서 originalPrefab 확인
    ↓
SetActive(false) 및 부모 오브젝트로 이동
    ↓
Enqueue()로 해당 풀에 다시 추가</code></pre><h2 id="성능-최적화-팁">성능 최적화 팁</h2>
<h3 id="1-적절한-풀-크기-설정">1. 적절한 풀 크기 설정</h3>
<pre><code class="language-csharp">// 게임의 특성에 따라 풀 크기 조정
[SerializeField] private int bulletPoolSize = 50;    // 빈번한 생성
[SerializeField] private int enemyPoolSize = 20;     // 중간 빈도
[SerializeField] private int effectPoolSize = 30;    // 이펙트용</code></pre>
<h3 id="2-풀-예열pre-warming">2. 풀 예열(Pre-warming)</h3>
<pre><code class="language-csharp">void Start()
{
    // 게임 시작 시 자주 사용되는 오브젝트들을 미리 생성
    PrewarmPool(bulletPrefab, 50);
    PrewarmPool(enemyPrefab, 20);
}

void PrewarmPool(GameObject prefab, int count)
{
    for (int i = 0; i &lt; count; i++)
    {
        GameObject obj = ObjectPool.instance.GetObject(prefab);
        ObjectPool.instance.ReturnObject(obj, 0f);
    }
}</code></pre>
<h3 id="3-풀-크기-모니터링">3. 풀 크기 모니터링</h3>
<pre><code class="language-csharp">public void LogPoolStatus()
{
    foreach (var kvp in poolDictionary)
    {
        Debug.Log($&quot;Pool {kvp.Key.name}: {kvp.Value.Count} objects available&quot;);
    }
}</code></pre>
<h2 id="주의사항-및-베스트-프랙티스">주의사항 및 베스트 프랙티스</h2>
<h3 id="1-오브젝트-초기화">1. 오브젝트 초기화</h3>
<p>풀에서 가져온 오브젝트는 이전 상태를 유지할 수 있으므로, 사용 전 반드시 초기화해야 합니다.</p>
<pre><code class="language-csharp">public class Bullet : MonoBehaviour
{
    void OnEnable()
    {
        // 풀에서 활성화될 때마다 초기화
        GetComponent&lt;Rigidbody&gt;().velocity = Vector3.zero;
        transform.localScale = Vector3.one;
        // 기타 초기화 로직...
    }
}</code></pre>
<h3 id="2-메모리-누수-방지">2. 메모리 누수 방지</h3>
<pre><code class="language-csharp">public void ClearPool(GameObject prefab)
{
    if (poolDictionary.ContainsKey(prefab))
    {
        while (poolDictionary[prefab].Count &gt; 0)
        {
            DestroyImmediate(poolDictionary[prefab].Dequeue());
        }
        poolDictionary.Remove(prefab);
    }
}</code></pre>
<h3 id="3-동적-풀-크기-조정">3. 동적 풀 크기 조정</h3>
<pre><code class="language-csharp">public void AdjustPoolSize(GameObject prefab, int newSize)
{
    if (!poolDictionary.ContainsKey(prefab)) return;

    Queue&lt;GameObject&gt; pool = poolDictionary[prefab];

    while (pool.Count &gt; newSize)
    {
        DestroyImmediate(pool.Dequeue());
    }

    while (pool.Count &lt; newSize)
    {
        CreateNewObject(prefab);
    }
}</code></pre>
<h2 id="고급-기능-확장">고급 기능 확장</h2>
<h3 id="1-타입별-풀-관리자">1. 타입별 풀 관리자</h3>
<pre><code class="language-csharp">[System.Serializable]
public class PoolInfo
{
    public GameObject prefab;
    public int poolSize;
    public bool expandable = true;
}

public class AdvancedObjectPool : MonoBehaviour
{
    [SerializeField] private PoolInfo[] poolsToCreate;

    void Start()
    {
        foreach (var poolInfo in poolsToCreate)
        {
            CreatePool(poolInfo);
        }
    }
}</code></pre>
<h3 id="2-이벤트-시스템-통합">2. 이벤트 시스템 통합</h3>
<pre><code class="language-csharp">public class ObjectPool : MonoBehaviour
{
    public System.Action&lt;GameObject&gt; OnObjectSpawned;
    public System.Action&lt;GameObject&gt; OnObjectReturned;

    public GameObject GetObject(GameObject prefab)
    {
        // ... 기존 로직 ...

        OnObjectSpawned?.Invoke(objectToSpawn);
        return objectToSpawn;
    }
}</code></pre>
<h2 id="결론">결론</h2>
<p>오브젝트 풀링 시스템은 Unity 게임의 성능을 크게 향상시킬 수 있는 필수 기술입니다. Dictionary를 활용한 효율적인 자료구조 설계와 Queue를 통한 FIFO(First In, First Out) 관리 방식은 메모리 효율성과 성능 최적화를 동시에 달성할 수 있게 해줍니다.</p>
<p>핵심은 <strong>적절한 풀 크기 설정</strong>, <strong>올바른 오브젝트 초기화</strong>, 그리고 <strong>메모리 관리</strong>입니다. 이 시스템을 기반으로 게임의 특성에 맞는 커스터마이징을 통해 더욱 효율적인 게임을 개발할 수 있을 것입니다.</p>
<p>오브젝트 풀링은 단순한 최적화 기법을 넘어서, 안정적이고 예측 가능한 게임 퍼포먼스를 보장하는 핵심 아키텍처 패턴입니다. 이를 마스터하여 보다 전문적인 게임 개발자로 성장하시기 바랍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity 피벗 포인트 문제와 해결 방법]]></title>
            <link>https://velog.io/@ch_kang/Unity-%ED%94%BC%EB%B2%97-%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EB%AC%B8%EC%A0%9C%EC%99%80-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@ch_kang/Unity-%ED%94%BC%EB%B2%97-%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EB%AC%B8%EC%A0%9C%EC%99%80-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 05 Aug 2025 04:57:50 GMT</pubDate>
            <description><![CDATA[<h1 id="unity-피벗-포인트-문제와-해결-방법">Unity 피벗 포인트 문제와 해결 방법</h1>
<p>Unity에서 3D 오브젝트를 다루다 보면 피벗 포인트(Pivot Point) 때문에 예상치 못한 문제들이 발생할 수 있습니다. 특히 레벨 디자인이나 조준 시스템을 구현할 때 피벗 포인트의 위치는 매우 중요한 요소가 됩니다. 이번 글에서는 피벗 포인트로 인한 문제점들과 이를 해결하는 다양한 방법을 알아보겠습니다.</p>
<h2 id="피벗-포인트란">피벗 포인트란?</h2>
<p>피벗 포인트는 Unity에서 오브젝트의 <strong>기준점</strong>입니다. <code>Transform.position</code>이 바로 이 피벗 포인트의 위치를 나타내며, 오브젝트의 회전, 스케일링, 이동의 중심축 역할을 합니다.</p>
<h3 id="unity-에디터에서-피벗-포인트-확인하기">Unity 에디터에서 피벗 포인트 확인하기</h3>
<p>Scene 뷰 상단의 툴바에서 <strong>Center</strong>와 <strong>Pivot</strong> 사이를 전환할 수 있습니다:</p>
<ul>
<li><strong>Center</strong>: 오브젝트의 기하학적 중심점 표시</li>
<li><strong>Pivot</strong>: 실제 피벗 포인트 위치 표시</li>
</ul>
<h2 id="피벗-포인트로-인한-주요-문제들">피벗 포인트로 인한 주요 문제들</h2>
<h3 id="1-레벨-디자인-시-오브젝트-배치-문제">1. 레벨 디자인 시 오브젝트 배치 문제</h3>
<p>가장 흔한 문제는 <strong>오브젝트 배치 시 발생하는 불편함</strong>입니다.</p>
<h4 id="문제-상황">문제 상황</h4>
<pre><code>- Cube (Unity 기본 오브젝트): 피벗 포인트가 중심에 위치
- Barrel (외부 에셋): 피벗 포인트가 하단에 위치</code></pre><p><strong>Cube의 경우:</strong></p>
<ul>
<li>바닥에 배치하면 절반이 땅속으로 들어감</li>
<li>플랫폼 위에 놓으면 중간 높이에 떠있게 됨</li>
<li>정확한 위치 조정을 위해 수동으로 Y값을 계속 조정해야 함</li>
</ul>
<p><strong>Barrel의 경우:</strong></p>
<ul>
<li>피벗 포인트가 하단에 있어 바닥에 자연스럽게 배치됨</li>
<li>레벨 디자인 시 훨씬 직관적</li>
</ul>
<h3 id="2-조준-시스템에서의-부정확성">2. 조준 시스템에서의 부정확성</h3>
<p>조준 시스템에서 <code>Transform.position</code>을 타겟으로 사용할 때:</p>
<pre><code class="language-csharp">// 현재 조준 시스템 (문제가 있는 코드)
aimPosition = target.transform.position;</code></pre>
<p><strong>결과:</strong></p>
<ul>
<li>피벗이 중심에 있는 오브젝트: 중앙을 조준</li>
<li>피벗이 하단에 있는 오브젝트: 바닥을 조준</li>
<li>일관성 없는 조준으로 게임플레이 품질 저하</li>
</ul>
<h3 id="3-회전-시-예상과-다른-동작">3. 회전 시 예상과 다른 동작</h3>
<p>피벗 포인트가 잘못된 위치에 있으면:</p>
<ul>
<li>오브젝트가 이상한 축을 중심으로 회전</li>
<li>바퀴나 기어 같은 회전 오브젝트에서 특히 문제 발생</li>
</ul>
<h2 id="해결-방법들">해결 방법들</h2>
<h3 id="1-3d-모델링-단계에서-해결-근본적-해결">1. 3D 모델링 단계에서 해결 (근본적 해결)</h3>
<p><strong>Blender 등에서 피벗 포인트 조정:</strong></p>
<ul>
<li>장점: 완전한 해결, 추가 작업 불필요</li>
<li>단점: 에셋 수정 권한 필요, 기존 에셋 활용 어려움</li>
</ul>
<h3 id="2-부모-오브젝트를-활용한-해결">2. 부모 오브젝트를 활용한 해결</h3>
<pre><code>Parent GameObject (빈 오브젝트)
└── Child GameObject (실제 모델)</code></pre><p><strong>구현 방법:</strong></p>
<ol>
<li>빈 게임오브젝트 생성 (부모)</li>
<li>실제 모델을 자식으로 배치</li>
<li>자식 오브젝트의 위치를 조정하여 부모의 피벗을 원하는 위치로 설정</li>
<li>타겟 컴포넌트, 콜라이더 등은 부모에 설정</li>
</ol>
<p><strong>장점:</strong></p>
<ul>
<li>기존 에셋 수정 없이 해결</li>
<li>완전한 제어 가능</li>
</ul>
<p><strong>단점:</strong></p>
<ul>
<li>모든 오브젝트에 수동 작업 필요</li>
<li>오브젝트 계층 복잡해짐</li>
</ul>
<h3 id="3-스크립트를-통한-동적-해결-권장">3. 스크립트를 통한 동적 해결 (권장)</h3>
<p>가장 효율적인 해결 방법은 <strong>Renderer의 bounds.center</strong>를 활용하는 것입니다.</p>
<pre><code class="language-csharp">public class PlayerAim : MonoBehaviour
{
    private void UpdateAimPosition()
    {
        if (target != null)
        {
            // Renderer 컴포넌트가 있는지 확인
            Renderer targetRenderer = target.GetComponent&lt;Renderer&gt;();

            if (targetRenderer != null)
            {
                // 렌더러의 바운드 중심점을 조준점으로 설정
                aimPosition = targetRenderer.bounds.center;
            }
            else
            {
                // Renderer가 없다면 기존 방식 사용
                aimPosition = target.transform.position;
            }
        }
    }
}</code></pre>
<p><strong>이 방법의 장점:</strong></p>
<ul>
<li>코드 한 줄로 해결</li>
<li>모든 오브젝트에 자동 적용</li>
<li>에셋 수정 불필요</li>
<li>성능 영향 최소</li>
</ul>
<h3 id="4-개선된-범용-타겟-시스템">4. 개선된 범용 타겟 시스템</h3>
<p>더 정교한 시스템을 원한다면:</p>
<pre><code class="language-csharp">public class SmartTargetSystem : MonoBehaviour
{
    [System.Serializable]
    public class TargetInfo
    {
        public Transform target;
        public Vector3 customOffset = Vector3.zero;
        public bool useCustomOffset = false;
    }

    public Vector3 GetTargetPosition(Transform target)
    {
        // 1. 커스텀 타겟 포인트 확인
        TargetPoint customTarget = target.GetComponent&lt;TargetPoint&gt;();
        if (customTarget != null)
        {
            return customTarget.GetTargetPosition();
        }

        // 2. Renderer bounds 중심 사용
        Renderer renderer = target.GetComponent&lt;Renderer&gt;();
        if (renderer != null)
        {
            return renderer.bounds.center;
        }

        // 3. Collider 중심 사용
        Collider col = target.GetComponent&lt;Collider&gt;();
        if (col != null)
        {
            return col.bounds.center;
        }

        // 4. 기본값: Transform 위치
        return target.position;
    }
}

// 특별한 타겟 포인트가 필요한 오브젝트용 컴포넌트
public class TargetPoint : MonoBehaviour
{
    [SerializeField] private Vector3 localOffset = Vector3.zero;

    public Vector3 GetTargetPosition()
    {
        return transform.position + transform.TransformDirection(localOffset);
    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(GetTargetPosition(), 0.1f);
    }
}</code></pre>
<h2 id="실제-적용-사례">실제 적용 사례</h2>
<h3 id="레벨-디자인-워크플로우-개선">레벨 디자인 워크플로우 개선</h3>
<pre><code class="language-csharp">[System.Serializable]
public class PropPlacementHelper : MonoBehaviour
{
    [Header(&quot;Auto-Snap to Ground&quot;)]
    public bool snapToGround = true;
    public LayerMask groundLayer = 1;

    private void Start()
    {
        if (snapToGround)
        {
            SnapToGround();
        }
    }

    private void SnapToGround()
    {
        Renderer renderer = GetComponent&lt;Renderer&gt;();
        if (renderer != null)
        {
            Vector3 bottomPoint = renderer.bounds.min;

            if (Physics.Raycast(transform.position, Vector3.down, out RaycastHit hit, Mathf.Infinity, groundLayer))
            {
                float offset = transform.position.y - bottomPoint.y;
                transform.position = new Vector3(transform.position.x, hit.point.y + offset, transform.position.z);
            }
        }
    }
}</code></pre>
<h2 id="성능-고려사항">성능 고려사항</h2>
<p><code>Renderer.bounds</code>는 매 프레임 계산할 필요가 없으므로:</p>
<pre><code class="language-csharp">public class OptimizedTargetSystem : MonoBehaviour
{
    private Dictionary&lt;Transform, Vector3&gt; cachedTargetPositions = new Dictionary&lt;Transform, Vector3&gt;();

    public Vector3 GetCachedTargetPosition(Transform target)
    {
        if (!cachedTargetPositions.ContainsKey(target))
        {
            CacheTargetPosition(target);
        }

        return cachedTargetPositions[target];
    }

    private void CacheTargetPosition(Transform target)
    {
        Renderer renderer = target.GetComponent&lt;Renderer&gt;();
        Vector3 targetPos = renderer != null ? renderer.bounds.center : target.position;
        cachedTargetPositions[target] = targetPos;
    }
}</code></pre>
<h2 id="결론">결론</h2>
<p>피벗 포인트 문제는 Unity 개발에서 자주 마주치는 일반적인 이슈입니다. 가장 효율적인 해결책은 <strong>Renderer.bounds.center를 활용한 스크립트 기반 접근</strong>입니다. 이 방법은:</p>
<ul>
<li>최소한의 코드로 최대 효과</li>
<li>기존 에셋 활용 가능</li>
<li>자동화된 해결책</li>
<li>확장 가능한 시스템</li>
</ul>
<p>Unity 개발의 핵심은 <strong>문제 해결 능력</strong>입니다. 피벗 포인트 같은 기본적인 이슈부터 시작해서 점진적으로 복잡한 문제들을 해결하는 경험을 쌓아가시기 바랍니다. 이러한 문제 해결 스킬은 게임 개발뿐만 아니라 실생활에서도 큰 도움이 될 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity Multi-Aim Constraint를 활용한 캐릭터 조준 시스템 구현]]></title>
            <link>https://velog.io/@ch_kang/Unity-Multi-Aim-Constraint%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%BA%90%EB%A6%AD%ED%84%B0-%EC%A1%B0%EC%A4%80-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@ch_kang/Unity-Multi-Aim-Constraint%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%BA%90%EB%A6%AD%ED%84%B0-%EC%A1%B0%EC%A4%80-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 04 Aug 2025 02:06:05 GMT</pubDate>
            <description><![CDATA[<h1 id="unity-multi-aim-constraint를-활용한-캐릭터-조준-시스템-구현">Unity Multi-Aim Constraint를 활용한 캐릭터 조준 시스템 구현</h1>
<p>Unity의 Animation Rigging 패키지를 사용하면 캐릭터가 특정 타겟을 자연스럽게 바라보도록 할 수 있습니다. 이번 글에서는 Multi-Aim Constraint를 활용해 머리와 총 조준 시스템을 구현하는 방법을 알아보겠습니다.</p>
<h2 id="필요한-준비물">필요한 준비물</h2>
<ul>
<li>Unity Animation Rigging 패키지</li>
<li>Humanoid 애니메이션이 적용된 캐릭터 모델</li>
<li>무기 모델 (선택사항)</li>
</ul>
<h2 id="1-머리-조준-시스템-구현">1. 머리 조준 시스템 구현</h2>
<h3 id="1-1-head-aim-오브젝트-생성">1-1. Head Aim 오브젝트 생성</h3>
<p>먼저 리그 시스템을 위한 빈 오브젝트를 생성합니다.</p>
<pre><code>GameObject → Create Empty
이름: &quot;Head Aim&quot;</code></pre><h3 id="1-2-multi-aim-constraint-컴포넌트-추가">1-2. Multi-Aim Constraint 컴포넌트 추가</h3>
<ol>
<li>Head Aim 오브젝트를 선택</li>
<li><strong>Add Component</strong> 클릭</li>
<li><strong>Multi-Aim Constraint</strong> 검색 후 추가</li>
</ol>
<h3 id="1-3-제약-조건-설정">1-3. 제약 조건 설정</h3>
<p><strong>Constrained Object 설정:</strong></p>
<ul>
<li>캐릭터의 Head 본을 Constrained Object 필드에 드래그</li>
</ul>
<p><strong>조준 축 확인:</strong></p>
<ul>
<li>Head 본을 선택한 상태에서 &#39;W&#39; 키를 눌러 기즈모 확인</li>
<li>일반적으로 Z축이 전방(Forward), Y축이 상방(Up)</li>
</ul>
<p><strong>소스 오브젝트 설정:</strong></p>
<ul>
<li>Source Objects의 &#39;+&#39; 버튼 클릭</li>
<li>타겟 게임 오브젝트를 드래그하여 설정</li>
</ul>
<h3 id="1-4-테스트">1-4. 테스트</h3>
<p>Animation 창에서 <strong>Preview</strong> 모드를 활성화하여 머리가 타겟을 향해 회전하는지 확인합니다.</p>
<h2 id="2-총-조준-시스템-구현">2. 총 조준 시스템 구현</h2>
<h3 id="2-1-gun-aim-오브젝트-생성">2-1. Gun Aim 오브젝트 생성</h3>
<pre><code>GameObject → Create Empty
이름: &quot;Gun Aim&quot;</code></pre><h3 id="2-2-multi-aim-constraint-설정">2-2. Multi-Aim Constraint 설정</h3>
<p>Gun Aim 오브젝트에 Multi-Aim Constraint 컴포넌트를 추가합니다.</p>
<h3 id="2-3-오른손-본-설정">2-3. 오른손 본 설정</h3>
<p><strong>Constrained Object:</strong></p>
<ul>
<li>캐릭터의 Right Hand 본을 설정</li>
<li>무기 홀더가 있다면 해당 오브젝트를 Right Hand 본 하위에 배치</li>
</ul>
<p><strong>조준 축 확인:</strong></p>
<ul>
<li>Right Hand 본 선택 후 &#39;W&#39; 키로 기즈모 확인</li>
<li>일반적으로 Y축이 전방, -X축이 상방</li>
</ul>
<p><strong>Source Objects:</strong></p>
<ul>
<li>동일한 타겟 오브젝트를 설정</li>
</ul>
<h3 id="2-4-무기-정렬-조정">2-4. 무기 정렬 조정</h3>
<p>무기와 손의 정렬이 맞지 않을 경우, 무기 홀더의 회전값을 조정합니다:</p>
<pre><code>예시 회전값:
X: -90
Y: 0
Z: 0</code></pre><h2 id="3-스크립트를-통한-타겟-위치-동적-설정">3. 스크립트를 통한 타겟 위치 동적 설정</h2>
<p>마우스 위치나 특정 타겟에 따라 조준점을 동적으로 변경하려면 다음과 같은 스크립트를 사용할 수 있습니다:</p>
<pre><code class="language-csharp">public class AimController : MonoBehaviour
{
    public Transform aimTarget;

    void Update()
    {
        // 캐릭터 위치 기준으로 조준점 설정
        Vector3 aimPosition = new Vector3(
            transform.position.x,
            transform.position.y + 1f, // 캐릭터보다 약간 위
            transform.position.z
        );

        aimTarget.position = aimPosition;
    }
}</code></pre>
<h2 id="4-활용-예시">4. 활용 예시</h2>
<p>이 조준 시스템은 다음과 같은 상황에서 활용할 수 있습니다:</p>
<ul>
<li><strong>NPC 시선 처리</strong>: 플레이어가 NPC 근처에 오면 시선을 돌리는 효과</li>
<li><strong>보안 카메라</strong>: 플레이어를 추적하는 카메라 시스템</li>
<li><strong>적 AI</strong>: 플레이어를 조준하는 적 캐릭터</li>
<li><strong>상호작용</strong>: 특정 오브젝트를 바라보는 캐릭터</li>
</ul>
<h2 id="5-주의사항-및-팁">5. 주의사항 및 팁</h2>
<h3 id="preview-모드-사용">Preview 모드 사용</h3>
<ul>
<li>제약 조건 변경 후에는 반드시 Preview 모드를 다시 활성화해야 변경사항이 적용됩니다</li>
</ul>
<h3 id="타겟-위치-조정">타겟 위치 조정</h3>
<ul>
<li>타겟이 너무 낮으면 캐릭터가 아래를 보게 되므로, 캐릭터 높이에 맞춰 조정이 필요합니다</li>
</ul>
<h3 id="정확도-vs-성능">정확도 vs 성능</h3>
<ul>
<li>완벽한 정확도보다는 게임플레이에 충분한 수준에서 타협점을 찾는 것이 좋습니다</li>
<li>총알 분산 등의 시스템이 있다면 약간의 부정확성은 문제가 되지 않습니다</li>
</ul>
<h3 id="디버깅">디버깅</h3>
<ul>
<li>무기 홀더에 작은 큐브를 생성하여 조준 방향을 시각적으로 확인할 수 있습니다</li>
</ul>
<h2 id="마치며">마치며</h2>
<p>Multi-Aim Constraint를 활용하면 복잡한 수학 계산 없이도 자연스러운 조준 시스템을 구현할 수 있습니다. 이 시스템을 기반으로 더 복잡한 애니메이션 로직을 추가하여 게임의 몰입감을 높여보세요.</p>
<p>Unity의 Animation Rigging 시스템은 이외에도 다양한 제약 조건을 제공하므로, 프로젝트의 요구사항에 맞는 적절한 컴포넌트를 선택하여 사용하시기 바랍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Awake()와 WaitForEndOfFrame()를 같이 쓰지말자.]]></title>
            <link>https://velog.io/@ch_kang/Awake%EC%99%80-WaitForEndOfFrame%EB%A5%BC-%EA%B0%99%EC%9D%B4-%EC%93%B0%EC%A7%80%EB%A7%90%EC%9E%90</link>
            <guid>https://velog.io/@ch_kang/Awake%EC%99%80-WaitForEndOfFrame%EB%A5%BC-%EA%B0%99%EC%9D%B4-%EC%93%B0%EC%A7%80%EB%A7%90%EC%9E%90</guid>
            <pubDate>Tue, 24 Jun 2025 00:38:26 GMT</pubDate>
            <description><![CDATA[<p>게임 과제 중 비트세이버를 클론코딩하는 과제가 있었다.
그 중 박스를 휘두를 경우 오브젝트가 둘로 나뉘는 효과를 주고 싶어 찾아보았다.</p>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/655c28a4-f87f-4906-be3e-aa330e75200f/image.png" alt=""></p>
<p>다음과 같은 효과를 주고싶었고</p>
<p><a href="https://www.youtube.com/watch?v=GQzW6ZJFQ94&amp;t=898s">https://www.youtube.com/watch?v=GQzW6ZJFQ94&amp;t=898s</a></p>
<p>다음 유튜브 영상을 참고하여 구현하던 중 잘 적용될때도 있고 안될 때도 있는 이슈를 발견하였다.</p>
<p>로그를 찍어보며 찾아보다가, </p>
<p>프레임을 저장하고 칼의 속도를 측정하는 스크립트에서 코루틴이 비정상적임을 발견하였다.</p>
<p>Awake() 함수에서 코루틴 함수를 호출하고
코루틴은 yield return new WaitForEndOfFrame(); 라는 함수를 호출하고 있었다.</p>
<p>여기서 문제는 WaitForEndOfFrame() 이 함수가 Awake() 함수에서 사용시 문제가 발생할 수 있다는 것이었다.</p>
<p>실제 문제:
Unity 커뮤니티에서 보고된 실제 문제는 WaitForEndOfFrame()이 특정 상황에서 예상과 다르게 동작하는 것으로, 이는 렌더링 파이프라인과 프레임 타이밍의 복잡한 상호작용 때문입니다. Unity DiscussionsUnity Discussions 특히 Unity Tests나 배치 모드에서 WaitForEndOfFrame()이 제대로 동작하지 않는 것으로 알려져 있습니다. Do not use WaitForEndOfFrame - Unity Engine - Unity Discussions
올바른 해결 방법:
Awake()에서 Start()로 코루틴을 이동시키는 것은 좋은 해결책입니다. 왜냐하면 &quot;모든 객체의 Awake와 OnEnable이 완료된 후 Start가 호출되기 때문&quot;입니다.
관련 링크:</p>
<p>Unity 공식 실행 순서 문서
WaitForEndOfFrame 실행 순서 문제
Unity 스크립트 실행 순서 버그</p>
<p>결론: 문제 해결 방법은 맞지만, 원인 분석에서 카메라 초기화와 관련된 부분은 정확하지 않습니다. 실제로는 Unity의 스크립트 실행 순서와 WaitForEndOfFrame()의 내부 동작 방식의 문제입니다.</p>
<p>그렇다면, Awake() 될때 카메라가 인스턴스화가 되기전에 WaitForEndOfFrame()함수가 실행 되지 않을 수 있다는 가능성을 이야기한다.</p>
<p>따라서, 해결방법은 Awake에 있던 Corutine 함수를 Start로 옮겨 주었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RAG를 활용한 LLM Application 개발 (feat. LangChain) (Lecture)]]></title>
            <link>https://velog.io/@ch_kang/RAG%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-LLM-Application-%EA%B0%9C%EB%B0%9C-feat.-LangChain-Lecture</link>
            <guid>https://velog.io/@ch_kang/RAG%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-LLM-Application-%EA%B0%9C%EB%B0%9C-feat.-LangChain-Lecture</guid>
            <pubDate>Wed, 11 Dec 2024 03:46:05 GMT</pubDate>
            <description><![CDATA[<p>강의 주소는 <a href="https://www.inflearn.com/course/rag-llm-application%EA%B0%9C%EB%B0%9C-langchain/dashboard">여기</a>
깃허브 주소는 <a href="https://github.com/Kangchanghwan/inflearn-llm-lecture">여기</a>
<img src="https://velog.velcdn.com/images/ch_kang/post/1422ddf8-5958-4652-ba19-9bc3b8628ec7/image.png" alt=""></p>
<hr>
<h3 id="개요">개요</h3>
<p>대 AI 시대 이 강의를 듣기 이전부터 수십번 생각했던 LLM(Large Language Model) 구축. 이 강의로 실현했다. &quot;나만의 챗봇을 만들 수는 없을까? 내가 어떤 문서를 주면 알아서 학습하고 문서를 참고해서 답을 주면 좋겠어&quot;라는 니즈를 해결할 강의였다. </p>
<hr>
<h3 id="시스템-구조">시스템 구조</h3>
<p>USER QUESTION<br>     ↓<br><strong>Dictionary Chain</strong><br>     ↓<br><strong>Modified/Original Question</strong><br>     ↓  </p>
<hr>
<table>
<thead>
<tr>
<th>RAG Chain</th>
</tr>
</thead>
<tbody><tr>
<td></td>
</tr>
<tr>
<td>History Retriever</td>
</tr>
<tr>
<td></td>
</tr>
<tr>
<td>v</td>
</tr>
<tr>
<td>Reformulated Question</td>
</tr>
<tr>
<td></td>
</tr>
<tr>
<td>QA Chain</td>
</tr>
<tr>
<td></td>
</tr>
<tr>
<td>v</td>
</tr>
<tr>
<td>Final Answer</td>
</tr>
</tbody></table>
<hr>
<p>↓  
<strong>Conversation History Updated</strong><br>     ↓<br><strong>FINAL RESPONSE TO USER</strong>  </p>
<hr>
<h3 id="알게된-점">알게된 점</h3>
<ol>
<li>LLM 상용 제품들은 더 복잡한 알고리즘을 거치겠지만, 큰틀은 비슷할 것이라 생각했다.</li>
<li>데이터를 벡터DB에 넣고 최근접알고리즘으로 분류하는 과정을 보며 질문하는것도 중요하지만 데이터를 선처리 하는 과정이 더 중요하겠다 생각하였다.</li>
<li>파이선을 이번에 처음 사용하게 되었는데 잘사용할 수 있으면 참 좋은 언어겠구나 생각하게되었다.(노트북으로 단위 실행이 가능한게 편했음)  </li>
</ol>
<hr>
<h3 id="하고싶은-것">하고싶은 것</h3>
<ol>
<li>스프링으로 이거 구현 안되나? 실제로 구현해 보았는데 프론트가 없다. (코드는 <a href="https://github.com/Kangchanghwan/tax-chat-bot">여기</a>)<ul>
<li>구현하면서 느낀건데 생각보다 레퍼런스가 없고 VectorDB같은 경우 안되는 경우도 있었다.</li>
</ul>
</li>
<li>스프링으로 구현한거를 조금 변형해서 문서를 넣으면 알아서 splite 및 벡터화 시키고, 벡터화 된 데이터를 가지고 질의해주는 LLM을 만들고 싶다. </li>
<li>세법을 가지고 테스트 했는데 워드문서 내에는 사진으로 된 표들이 여러개 있었다. 이것을 chatGPT가 이해 할 수 있게 자동으로 Markdown언어로 바꿔주는 기능을 만들고 싶다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[견고한 결제 시스템 구축 (Lecture)]]></title>
            <link>https://velog.io/@ch_kang/%EA%B2%AC%EA%B3%A0%ED%95%9C-%EA%B2%B0%EC%A0%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95-Lecture</link>
            <guid>https://velog.io/@ch_kang/%EA%B2%AC%EA%B3%A0%ED%95%9C-%EA%B2%B0%EC%A0%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95-Lecture</guid>
            <pubDate>Tue, 10 Dec 2024 05:16:19 GMT</pubDate>
            <description><![CDATA[<p>강의 주소는 <a href="https://www.inflearn.com/course/%EA%B2%AC%EA%B3%A0%ED%95%9C-%EA%B2%B0%EC%A0%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95/dashboard">여기</a>
시스템 구조 이미지 저작권은 <a href="https://newsletter.pragmaticengineer.com/p/designing-a-payment-system">여기</a>
깃허브 소스는 <a href="https://github.com/Kangchanghwan/payment-repo">여기</a></p>
<hr>
<h3 id="개요">개요</h3>
<p>이전회사의 포인트 도메인은 해당 특성에서 요구하는 신뢰성과 일관성을 해결하기 위해 SQS에 모든 요청을 넣었다. 그러나 이는 동시성을 제한하므로 동시성에 대한 문제는 발생하지 않으나 성능문제를 낳았다. 1분당 10개의 리소스를 처리하지도 못하는 처참한 성능 문제였다. 이후로 계속해서 좋은 시스템은 무얼까 고민했다. 그러다가 다음 강의를 보았고 커리큘럼이 좋아서 선택했다.</p>
<hr>
<h3 id="강의를-시청한-이유">강의를 시청한 이유</h3>
<ol>
<li><p><strong>견고한 결제 시스템에 &#39;장부&#39; 도메인에 대한 궁금증</strong> 
(실 프로젝트에서도 필요하겠다 생각한 부분인데 어떻게 적용할지 잡히지 않아서 고민했던 기억이 있음)</p>
</li>
<li><p>*<em>헥사고날아키텍쳐의 기준 *</em>(홀로 공부하고 적용하니 &quot;이게 맞나&quot; 하는 부분이 많았음)</p>
</li>
<li><p><strong>Kotlin에 대한 관심</strong> (자바도 모르는데 코틀린이 과하다고 생각할 수 있지만 알아두고 배워두는건 좋다고 생각)</p>
</li>
<li><p><strong>kafka 기술 적용</strong> (이론적인 부분만 알고 있다가 실제 사용하며 이슈를 체크하고 싶었음)</p>
</li>
</ol>
<hr>
<h3 id="시스템-구조도">시스템 구조도</h3>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/a7c5f44f-c516-4d68-9f5c-d065b858839e/image.png" alt=""></p>
<hr>
<h3 id="알게-된-것">알게 된 것</h3>
<ol>
<li>헥사고날 아키텍쳐의 적용 기준 <ul>
<li>처음에 보면 코드가 복잡하다. 그러나 알고 보면 캡슐화와 기능분할이 잘 되어있다는 생각을 하게 된다. 그래서 팀원 간의 협업시에는 이해도가 100인 상태에서 같이 적용해야 적용이 가능한 부분이다. 대충 이해해서 적용해서는 안된다. 그러면 죽도 밥도 안된다.</li>
<li>간단한 프로젝트에는 적용하지 말자. 헥사고날은 많은 클래스를 만들게 된다. MVC 패키지로 적용해도 왠만한 프로젝트는 유지보수 할 수 있다. 그게 더 비용적으로도 효율적이다.</li>
<li>테스트가 간단했다. 레이어 별로 종합 테스트 하기도 편했다. 이는 헥사고날 아키텍쳐의 장점인 의존성 관리를 잘했기 때문이다.</li>
<li>영속화 계층 부분에서 의존관계를 어떻게 해야할지 많이 했갈렸는데 강의를 듣고 보니 명확해지는 부분이 분명히 있었다. (도메인 레이어에서 필요한 기능 별이 아닌 영속화 계층에서 구현가능한 기능별로)</li>
<li>보아하니 핵사고날 아키텍쳐를 사용한 경우 JPA가 편한 부분보다는 불편한 부분이 더 많더라. 차라리 로우쿼리를 사용하는 JDBC를 사용하는 것도 좋은 선택지다.   </li>
</ul>
</li>
</ol>
<ol start="2">
<li><p>Transactional Outbox Pattern을 구축해보았다. </p>
<ul>
<li>결제 승인 정보를 DB에 저장할때, 동일한 트랜젝션에서 메시지 큐에 전달할 이벤트들도 함께 데이터베이스에 저장하는 방법을 사용하였다. 이후 저장된 이벤트 메시지들을 가져와 메시지 큐로 전달하는 방식을 사용하면 이벤트 전달과 데이터베이스 반영 모두 성공가능한 시나리오가 된다.</li>
</ul>
</li>
<li><p>카프카의 원자적 처리에 대한 실습을 해보았다.</p>
<ul>
<li>카프카는 내부적으로 트랜젝션 기능을 이용할 수 있다는 것을 알았다. 이를 통해 메세지가 전달되고 처리되는 것을 보장 할 수 있다.</li>
</ul>
</li>
<li><p>Optimistic Locking을 사용해보았다.</p>
<ul>
<li>정산 금액이 동시에 업데이트 되는 경우를 방지하기 위해 사용하였다.</li>
</ul>
</li>
<li><p>Database Trigger 이용해보았다.</p>
<ul>
<li>장부 도메인에서 차변 대변 방식으로 기록할때 정확히 기록되었는지 확인하는 트리거</li>
<li>모든 결제 비즈니스 로직이 끝난 후 상태를 업데이트하는 트리거를 만들어 해결하였다.</li>
<li>모든 비즈니스로직을 코드로 해결할 필요가 없다. 쉽고 비용 효율적이면 그것을 선택하는 것도 개발자의 능력이다.</li>
</ul>
</li>
</ol>
<ol start="6">
<li>Webflux를 사용하였다 <ul>
<li>어렵다. 그러나 코드가 깔끔하고 성능을 기대해볼만 하다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="더-해보고-싶은-것">더 해보고 싶은 것</h3>
<ol>
<li>카프카의 원자적 처리를 Spring Modulith 라이브러리로 해결할 수 있을까?</li>
<li>성능 체크 </li>
<li>...</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring) Spring Cloud Config Client를 나만의 라이브러리로 만들어 보자.]]></title>
            <link>https://velog.io/@ch_kang/Spring-Spring-Cloud-Config-Client%EB%A5%BC-%EB%82%98%EB%A7%8C%EC%9D%98-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4-%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@ch_kang/Spring-Spring-Cloud-Config-Client%EB%A5%BC-%EB%82%98%EB%A7%8C%EC%9D%98-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4-%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 23 Mar 2023 11:37:46 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Spring Cloud Config를 동적으로 조작하는 방법은 크게 3가지로 나뉜다.</p>
</blockquote>
<h3 id="첫째--spring-config-client-에게-refresh-요청을-보내는-방법">첫째 , Spring Config Client 에게 refresh 요청을 보내는 방법</h3>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/c44a8383-2b61-451c-89fb-cf87555e11f0/image.png" alt=""></p>
<p>장점 : 구조가 단순해서 서버 관리가 쉽다. </p>
<p>단점 : 변경한 서비스의 인스턴스가 100개라 가정했을때 refresh 요청 보내야할게 100개다ㅎㅎ;</p>
<h3 id="둘째--spring-cloud-bus와-mq를-추가하는-방법">둘째 , Spring Cloud Bus와 MQ를 추가하는 방법</h3>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/2f05d2b3-0e5d-49d0-b93c-d9d020b2c2f3/image.png" alt=""></p>
<p>장점 : 인스턴스 별로 refresh를 MQ가 대신 날려주는 셈이다. 개꿀</p>
<p>단점 : config 서버 하나때문에 MQ를 두는 것은 잘 생각해봐야한다. 이미 MQ가 도입된 상황이면 진짜 개꿀이지만 그렇지 않은 경우라면 MQ서버도 관리해야 할 대상에 들어간다. </p>
<h3 id="셋째--첫번째-방법에-long-polling-방식을-도입하여-내-스스로-업데이트-하게하는-방법">셋째 , 첫번째 방법에 Long Polling 방식을 도입하여 내 스스로 업데이트 하게하는 방법.</h3>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/5aa13b50-f7f4-4cc2-a4fb-3f987145fbf8/image.png" alt=""></p>
<p>장점 : 자기스스로 업데이트 해서 속성이 변경되더라도 내가 안알려줘도 됨</p>
<p>단점 : 자꾸 서버에 물어봐야해서 리소스 낭비가 있음. 또, 잘 설정된 라이브러리를 헤집어서 설정해 줘야함..</p>
<p>-&gt; 첫번째 처럼 하기에는 너무 번거롭고 두번째 처럼 하기에는 서버 관리 비용이 들어 세번째로 정하였다.
리소스 낭비되는 부분은 Scheduling 시간을 널널하게 두면 되기 때문에 큰 비용이라 생각하지 않았고,설정이 굳이 실시간으로 이뤄져야 하는 이유를 찾지 못해서 세번째로 정하였다.</p>
<p>코드는 아래 주소에 잘 나와있어서 거의 비슷하게 했다.ㅋㅋㅋ</p>
<blockquote>
<p><a href="https://jaehun2841.github.io/2022/03/10/2022-03-11-spring-cloud-config-polling/">https://jaehun2841.github.io/2022/03/10/2022-03-11-spring-cloud-config-polling/</a> </p>
</blockquote>
<h1 id="문제-라고-하면-문제지요">문제!!!? 라고 하면 문제지요</h1>
<blockquote>
<p> 잘 설정된 라이브러리를 헤집는건 문제가 되지 않는다. 혼자 서비스를 개발하고 올린다면.. 
그러나!!! 개발해야할 서비스가 늘어나다 보면 각 서비스 별로 그 코드를 구현해 놓아야한다는 점이다. 그래서 생각했다. 이거 라이브러리로 만들면 한곳에서 관리하고 업데이트 할 수 있잖아!....
 나는 예전부터 나도 라이브러리 만들 수 있겠지 하는 조그마난 꿈도 있었기에 도전했다.</p>
</blockquote>
<p>라이브러리를 만드는 과정은 </p>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=tr5_OWgXDiw&amp;list=LL&amp;index=1&amp;t=680s">https://www.youtube.com/watch?v=tr5_OWgXDiw&amp;list=LL&amp;index=1&amp;t=680s</a></p>
</blockquote>
<p>이 동영상 토씨하나 안틀리고 따라하면 금방 만들 수 있더라...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Linux) vscode 에 EC2를 원격접속 시켜보자..]]></title>
            <link>https://velog.io/@ch_kang/Linux-vscode-%EC%97%90-EC2%EB%A5%BC-%EC%9B%90%EA%B2%A9%EC%A0%91%EC%86%8D-%EC%8B%9C%EC%BC%9C%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@ch_kang/Linux-vscode-%EC%97%90-EC2%EB%A5%BC-%EC%9B%90%EA%B2%A9%EC%A0%91%EC%86%8D-%EC%8B%9C%EC%BC%9C%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 05 Jan 2023 11:21:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<ul>
<li>사용하는 서버가 많을때 유용할거같다. </li>
</ul>
</blockquote>
<ul>
<li><p>파일 찾으려 ls -a 같은 문구 안써도 된다.. </p>
<p>우선 결과물 부터<br><img src="https://velog.velcdn.com/images/ch_kang/post/d9eed8ed-fccd-4a2e-a1ba-fc8136fde6b4/image.png" alt=""></p>
</li>
</ul>
<p>vscode 설치 와 EC2 서버 만드는 방법은 생략하고 vscode에 어떻게 연동시키는지만 알아보자</p>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/5fadb3e0-9172-4f1e-a0d2-5bbf355e0ccf/image.png" alt=""></p>
<ol>
<li>vscode  마켓플레이스에서 Remote를 검색하면 위 사진같은 플러그인을 다운받는다</li>
</ol>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/044ce810-0344-4b84-a397-d813904a25ed/image.png" alt=""></p>
<ol start="2">
<li>빨간 내모난 박스 안에 있는 버튼을 눌러 해당 화면이 나오는 곳으로 이동한다.</li>
</ol>
<pre><code class="language-shell">vim ~/.ssh/config </code></pre>
<ol start="3">
<li><p>커맨드 창에 위 내용을 입력해주고</p>
<pre><code class="language-shell">Host demo
HostName {EC2 PUBLIC IP}
User {사용자 이름}
IdentityFile {PEM 파일 위치}</code></pre>
</li>
<li><p>{  } 친 내용을 내 정보로 변경하여 입력해준다.</p>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/e172dd28-762d-4700-afa6-2f72579ef6b5/image.png" alt=""></p>
</li>
<li><p>위에 같이 연결할 수 있게 목록에 뜬다..</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring) @Transaction 은 언제 commit 할까?]]></title>
            <link>https://velog.io/@ch_kang/Spring-Transaction-%EC%9D%80-%EC%96%B8%EC%A0%9C-commit-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@ch_kang/Spring-Transaction-%EC%9D%80-%EC%96%B8%EC%A0%9C-commit-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 26 Dec 2022 11:53:05 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<pre><code class="language-java">  @Transactional
  public FriendProfileInfoRes updateTypeD(Long friendId, Long userId) {
    Friend friend = findFriends(friendId); // entity 조회
    FriendType beforeFriendType = friend.getType(); // 변경 전 데이터 저장   
    isMine(userId, friend); 
    friend.updateBlockToggle(); // 엔티티 속성 업데이트
    em.flush(); // 플러쉬를 통해 강제 업데이트
    em.clear(); // 컨텍스트 초기화
    Friend syncFriend = findFriends(friendId); // 변경된 엔티티 조회
    FriendProfileInfoRes friendProfileInfoRes = FriendProfileInfoRes.newInstance(syncFriend.getFriend(), syncFriend);
    if(!beforeFriendType.equals(syncFriend.getType())){ // 변경전 데이터와 변경된 엔티티의 데이터가 다르면 
      chatBlockFeignClient.chatBlocked( // 외부 api로 데이터 전송
        userId,
        ChatBlockReq
          .builder()
          .blockUserId(friend.getFriend().getId())
          .build());
    }
    return friendProfileInfoRes;
  }</code></pre>
<blockquote>
<p>@Transaction 어노테이션이 걸린 메소드 안에서 데이터 변경 후 외부 api로 데이터 변경되었음을 알렸지만, 외부 api로 데이터를 조회한 결과 변경되지 않고 조회되는 문제가 있었다.</p>
</blockquote>
<h2 id="원인찾기">원인찾기</h2>
<blockquote>
<ol>
<li>em.flush는 SQL을 발생시키지만 Commit을 시키지 않는 문제점 확인</li>
</ol>
</blockquote>
<h3 id="시도-1">시도 1</h3>
<pre><code class="language-java"> @Transactional
  public FriendProfileInfoRes updateTypeD(Long friendId, Long userId) {
    Friend friend = findFriends(friendId); // entity 조회
    FriendType beforeFriendType = friend.getType(); // 변경 전 데이터 저장
    isMine(userId, friend);
    //friend.updateBlockToggle(); // 엔티티 속성 업데이트
    if(friend.getType().equals(FriendType.G) || friend.getType().equals(FriendType.S))
      friendRepository.updateTypeD(friendId);
    if(friend.getType().equals(FriendType.D))
      friendRepository.updateTypeG(friendId);
    Friend syncFriend = findFriends(friendId); // 변경된 엔티티 조회
    FriendProfileInfoRes friendProfileInfoRes = FriendProfileInfoRes.newInstance(syncFriend.getFriend(), syncFriend);
    if(!beforeFriendType.equals(syncFriend.getType())){ // 변경전 데이터와 변경된 엔티티의 데이터가 다르면
      chatBlockFeignClient.chatBlocked( // 외부 api로 데이터 전송
        userId,
        ChatBlockReq
          .builder()
          .blockUserId(friend.getFriend().getId())
          .build());
    }
    return friendProfileInfoRes;
  }

  ////----------------------Repository-----------
  @Modifying(clearAutomatically = true, flushAutomatically = true)
  @Query(&quot;update Friend f set f.type = &#39;D&#39;, f.blockDate= now() where f.id = :id&quot;)
  void updateTypeD(Long id);
  @Modifying(clearAutomatically = true, flushAutomatically = true)
  @Query(&quot;update Friend f set f.type = &#39;G&#39;, f.blockDate= null where f.id = :id&quot;)
  void updateTypeG(Long id);
</code></pre>
<h3 id="시도-2">시도 2</h3>
<blockquote>
<ol start="2">
<li>여전히 업데이트가 되지 않는 문제가 확인되어 Transaction의 문제임을 판단하고 기존의 선언적 트랜젝션 방식이 아닌 명시적으로 선언해 줄 수 있는 방식을 채택</li>
</ol>
</blockquote>
<pre><code class="language-java"> public FriendProfileInfoRes updateTypeD(Long friendId, Long userId) {
    Friend friend = findFriends(friendId);
    FriendType beforeFriendType = friend.getType();
    isMine(userId, friend);
    TransactionDefinition definition = new DefaultTransactionDefinition();
    TransactionStatus status = platformTransactionManager.getTransaction(definition);
    try {
      if(friend.getType().equals(FriendType.G) || friend.getType().equals(FriendType.S))
        friendRepository.updateTypeD(friendId);
      if(friend.getType().equals(FriendType.D))
        friendRepository.updateTypeG(friendId);
      platformTransactionManager.commit(status);
    } catch (Exception e) {
      platformTransactionManager.rollback(status);
    }
    Friend syncFriend = findFriends(friendId);
    FriendProfileInfoRes friendProfileInfoRes = FriendProfileInfoRes.newInstance(syncFriend.getFriend(), syncFriend);
    if(!beforeFriendType.equals(syncFriend.getType())){
      chatBlockFeignClient.chatBlocked(
        userId,
        ChatBlockReq
          .builder()
          .blockUserId(friend.getFriend().getId())
          .build());
    }
    return friendProfileInfoRes;
  }</code></pre>
<h3 id="성공">성공</h3>
<blockquote>
<p>회고: Transaction안에서 flush를 하던 JPQL을 날리던간에 COMMIT이 되지 않아 데이터가 바로 업데이트 되지 않는 부분을 명확히 알 수 있었다.</p>
</blockquote>
<h3 id="리팩토링">리팩토링</h3>
<blockquote>
<p>외부 API 때문에  기존 서비스 로직이 크게 변경되었다. 서비스간 의존관계가 커진 것이다. 이런경우에는 TransactionalEventListener을 사용한 방식이나 리플렉션으로 해결해볼 수 있지 않을까? 하는 고민에서 리팩토링을 진행해본다.</p>
</blockquote>
<h2 id="transactionaleventlistener을-사용한-방식">TransactionalEventListener을 사용한 방식</h2>
<pre><code class="language-java">  public FriendProfileInfoRes updateTypeD(Long friendId, Long userId) {
    Friend friend = findFriends(friendId);
    isMine(userId, friend);
    friend.updateBlockToggle();
    applicationEventPublisher.publishEvent(
      new FriendBlockEvent(
        userId,
        ChatBlockReq.createInstance(friend.getFriend().getId()))
    );
    return FriendProfileInfoRes.newInstance(friend.getFriend(), friend);
  }</code></pre>
<blockquote>
<p>event Driven 방식으로 해결하니 기존 서비스 로직과의 결합이 느슨해졌다.
또한, @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 어노테이션을 걸어 줌으로써 데이터 동기화 이슈에 대해서도 해결할 수 있게 되었다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring) FeignClient로 카카오 로그인 구현]]></title>
            <link>https://velog.io/@ch_kang/Spring-FeignClient%EB%A1%9C-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@ch_kang/Spring-FeignClient%EB%A1%9C-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Wed, 09 Nov 2022 11:22:51 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<blockquote>
<p>의존 관계 때문에 <code>spring-boot-starter-oauth2-client</code> 를 사용하지 않고 Feign Client로 구현하고 싶었다.</p>
</blockquote>
<h1 id="배경">배경</h1>
<h3 id="나의-멀티모듈-프로젝트는-다음과-같은-구조를-가진다">나의 멀티모듈 프로젝트는 다음과 같은 구조를 가진다.</h3>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/07f219f7-dcd5-4880-b9e5-bd52482765ca/image.png" alt=""></p>
<h3 id="application--admin--batch-api-등-main을-포함한-jar파일들이-여기-속한다">Application : Admin , Batch, Api 등 main을 포함한 jar파일들이 여기 속한다.</h3>
<h3 id="system--jwtfilter-security-logconfig-등-application-들의-공통적이면서도-보조적인-수단들이-여기-속한다">System : JwtFilter, Security, LogConfig 등 Application 들의 공통적이면서도 보조적인 수단들이 여기 속한다.</h3>
<h3 id="domain--jpa-querydsl-등-db와-가장-가까히-소통하는-라이브러리가-여기속한다">Domain : JPA, QueryDsl 등 DB와 가장 가까히 소통하는 라이브러리가 여기속한다.</h3>
<h3 id="core--최소한의-라이브러리를-의존하면서-자바로-구현가능한-기능들이-여기-속한다">Core : 최소한의 라이브러리를 의존하면서 자바로 구현가능한 기능들이 여기 속한다.</h3>
<h3 id="infra--aws-feign-등-외부-라이브러리가-여기속한다">Infra : AWS, Feign 등 외부 라이브러리가 여기속한다.</h3>
<h1 id="문제점">문제점</h1>
<blockquote>
<p>OAuth2 라이브러리를 사용하게 되면 Spring Security가 Application 단계에서 구현되어 main을 가지는 각각의 프로젝트들에 적용해줘야하는 번거로움이 생긴다. 그렇게 된다면 multi-module을 할 필요가 없어지는 것이다. 따라서 의존성을 줄여주기 위해 FeignClient로 구현하였다.</p>
</blockquote>
<h1 id="해결">해결</h1>
<blockquote>
<p>RestTemplate 로 구현된 코드들이 꽤 있어서 구현에는 어렵지 않았지만 몇가지 트러블 슈팅이 있었다. 아래 코드를 보고 겪었던 트러블 슈팅을 포스팅 하겠다.</p>
</blockquote>
<h1 id="코드">코드</h1>
<h3 id="yml-kakao-developer를-참고하여-아래데이터를-가져온다">yml :kakao developer를 참고하여 아래데이터를 가져온다.</h3>
<pre><code class="language-yml">oauth2:
  kakao:
    infoUrl: https://kapi.kakao.com
    baseUrl: https://kauth.kakao.com
    clientId: 5d38e6dc1f62b10c9r3dc2e34fe6d24e62 
    redirectUri: http://localhost/api/v1/login/kakao/oauth2
    secretKey: I1nEw554k2oFM1n32P126Yro7NrRVU2G</code></pre>
<h3 id="kakaoinfo--yml-데이터를-가져온다">KakaoInfo : yml 데이터를 가져온다.</h3>
<pre><code class="language-java">package com.send.moduleinfra.feign.sns.kakao;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

@Component
@ConfigurationProperties(prefix = &quot;oauth2.kakao&quot;)
public class KakaoInfo {

  private String baseUrl;
  private String clientId;
  private String redirectUri;
  private String secretKey;

  public String kakaoUrlInit() {
    Map&lt;String, Object&gt; params = new HashMap&lt;&gt;();
    params.put(&quot;client_id&quot;, getClientId());
    params.put(&quot;redirect_uri&quot;, getRedirectUri());
    params.put(&quot;response_type&quot;, &quot;code&quot;);

    String paramStr = params.entrySet().stream()
      .map(param -&gt; param.getKey() + &quot;=&quot; + param.getValue())
      .collect(Collectors.joining(&quot;&amp;&quot;));

    return getBaseUrl()
      +&quot;/oauth/authorize&quot;
      + &quot;?&quot;
      + paramStr;
  }

  public void setBaseUrl(String baseUrl) {
    this.baseUrl = baseUrl;
  }

  public void setClientId(String clientId) {
    this.clientId = clientId;
  }

  public void setRedirectUri(String redirectUri) {
    this.redirectUri = redirectUri;
  }

  public String getBaseUrl() {
    return baseUrl;
  }

  public String getClientId() {
    return clientId;
  }

  public String getRedirectUri() {
    return redirectUri;
  }

  public String getSecretKey() {
    return secretKey;
  }

  public void setSecretKey(String secretKey) {
    this.secretKey = secretKey;
  }
}</code></pre>
<h3 id="controller--client가-kakao-로그인을-눌렀을-때-접근할-endpoint">Controller : client가 kakao 로그인을 눌렀을 때 접근할 endpoint</h3>
<pre><code class="language-java">  @GetMapping(&quot;/login/kakao&quot;)
  public ResponseEntity&lt;Object&gt; kakaoLogin()  {
    HttpHeaders httpHeaders = accountService.kakaoLogin();
    return httpHeaders != null ?
      new ResponseEntity&lt;&gt;(httpHeaders,HttpStatus.SEE_OTHER):
      ResponseEntity.badRequest().build();
  }</code></pre>
<h3 id="service--header에-redirect-주소를-설정한다">Service : Header에 Redirect 주소를 설정한다.</h3>
<pre><code class="language-java">
public HttpHeaders kakaoLogin(){
    return createHttpHeader(kakaoInfo.kakaoUrlInit());
  }

 private static HttpHeaders createHttpHeader(String str) {
    try {
      URI uri = new URI(str);
      HttpHeaders httpHeaders = new HttpHeaders();
      httpHeaders.setLocation(uri);
      return httpHeaders;
    } catch (URISyntaxException e) {
      e.printStackTrace();
    }
    return null;
  }</code></pre>
<h2 id="여기까지-정상적으로-했다면-위주소로-social-login-page-접근이-가능할-것이다">여기까지 정상적으로 했다면 위주소로 Social Login Page 접근이 가능할 것이다.</h2>
<hr>
<h2 id="다음으로-로그인-성공-후-토큰을-발급받고-토큰으로-회원정보를-받아보자">다음으로 로그인 성공 후 토큰을 발급받고 토큰으로 회원정보를 받아보자</h2>
<h3 id="klogintokenreq--토큰을-요청할때-쓰는-data-객체이다">KLoginTokenReq : 토큰을 요청할때 쓰는 Data 객체이다.</h3>
<pre><code class="language-java">package com.send.moduleinfra.feign.sns.kakao.dto;


import com.send.moduleinfra.feign.sns.google.GoogleInfo;
import com.send.moduleinfra.feign.sns.kakao.KakaoInfo;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KLoginTokenReq {

  private String code;
  private String client_id;
  private String client_secret;
  private String redirect_uri;
  private final String grant_type = &quot;authorization_code&quot;;

  public static KLoginTokenReq newInstance(KakaoInfo googleInfo, String code){
    return KLoginTokenReq.builder()
      .client_id(googleInfo.getClientId())
      .client_secret(googleInfo.getSecretKey())
      .redirect_uri(googleInfo.getRedirectUri())
      .code(code)
      .build();
  }

// kakao는 Content-Type 을 application/x-www-form-urlencoded 로 받는다.
// FeignClient는 기본이 JSON으로 변경하니 아래처럼 데이터를 변환 후 보내야 한다.
  @Override
  public String toString() {
    return
      &quot;code=&quot; + code + &#39;&amp;&#39; +
      &quot;client_id=&quot; + client_id + &#39;&amp;&#39; +
      &quot;client_secret=&quot; + client_secret + &#39;&amp;&#39; +
      &quot;redirect_uri=&quot; + redirect_uri + &#39;&amp;&#39; +
      &quot;grant_type=&quot; + grant_type;
   }  

}
</code></pre>
<h3 id="klogintokenres--요청한-토큰-데이터를-받을-data객체이다">KLoginTokenRes : 요청한 토큰 데이터를 받을 Data객체이다.</h3>
<pre><code class="language-java">package com.send.moduleinfra.feign.sns.kakao.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class KLoginTokenRes {

  private String access_token; // 애플리케이션이 Google API 요청을 승인하기 위해 보내는 토큰
  private String expires_in;   // Access Token의 남은 수명
  private String refresh_token;    // 새 액세스 토큰을 얻는 데 사용할 수 있는 토큰
  private String scope;
  private String token_type;   // 반환된 토큰 유형(Bearer 고정)
  private String id_token;
  private String refresh_token_expires_in;

  public String getAccess_token() {
    return &quot;Bearer &quot;+access_token;
  }
}
</code></pre>
<h3 id="ktokeninfores--토큰으로-사용자-정보를-받을-data객체이다">KTokenInfoRes : 토큰으로 사용자 정보를 받을 Data객체이다.</h3>
<pre><code class="language-java">package com.send.moduleinfra.feign.sns.kakao.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class KTokenInfoRes {
  private String id;
  private String expires_in;
  private String app_id;
}</code></pre>
<h4 id="여기까지-dto세팅이-끝났다">여기까지 Dto세팅이 끝났다.</h4>
<hr>
<h4 id="다음은-feignclient-setting-이다">다음은 FeignClient Setting 이다.</h4>
<h3 id="kakaologinfeignclient--login-code를-가지고-token을-요청하는-feign이다">KakaoLoginFeignClient : Login code를 가지고 Token을 요청하는 Feign이다</h3>
<pre><code class="language-java">package com.send.moduleinfra.feign.sns.kakao;


import com.send.moduleinfra.feign.sns.kakao.config.KakaoFeignConfiguration;
import com.send.moduleinfra.feign.sns.kakao.dto.KLoginTokenRes;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;


@FeignClient(name = &quot;kakaoLoginFeignClient&quot;, url = &quot;${oauth2.kakao.baseUrl}&quot;, configuration = KakaoFeignConfiguration.class)
@Component
public interface KakaoLoginFeignClient {

  @PostMapping(value = &quot;/oauth/token&quot;)
  KLoginTokenRes getToken(
    @RequestBody String kLoginTokenReq);
}</code></pre>
<h3 id="kakaofeignconfiguration--앞서-말했다시피-토큰요청은-별도의-content-type이-필요하다-따라서-별도의-설정파일을-만들어-주었다">KakaoFeignConfiguration : 앞서 말했다시피 토큰요청은 별도의 content-type이 필요하다 따라서, 별도의 설정파일을 만들어 주었다.</h3>
<pre><code class="language-java">
package com.send.moduleinfra.feign.sns.kakao.config;

import com.send.moduleinfra.feign.config.FeignClientExceptionErrorDecoder;
import feign.Logger;
import feign.RequestInterceptor;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;

public class KakaoFeignConfiguration {
  @Bean
  public RequestInterceptor requestInterceptor() {
    return template -&gt; template.header(&quot;Content-Type&quot;, &quot;application/x-www-form-urlencoded&quot;);
  }
  @Bean
  public ErrorDecoder errorDecoder() {
    return  new FeignClientExceptionErrorDecoder();
  }

  @Bean
  Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
  }

}</code></pre>
<h3 id="kakaoinfofeignclient--token을-가지고-회원정보를-요청하는-feign이다">KakaoInfoFeignClient : Token을 가지고 회원정보를 요청하는 Feign이다.</h3>
<pre><code class="language-java">package com.send.moduleinfra.feign.sns.kakao;

import com.send.moduleinfra.feign.config.FeignClientConfiguration;
import com.send.moduleinfra.feign.sns.google.dto.GTokenInfoRes;
import com.send.moduleinfra.feign.sns.kakao.dto.KTokenInfoRes;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = &quot;kakaoInfoFeignClient&quot;, url = &quot;${oauth2.kakao.infoUrl}&quot;, configuration = FeignClientConfiguration.class)
@Component
public interface KakaoInfoFeignClient {

  @GetMapping(&quot;/v1/user/access_token_info&quot;)
  KTokenInfoRes getInfo(@RequestHeader(name = &quot;Authorization&quot;) String Authorization);
}</code></pre>
<h3 id="controller--여기서-endpoint는-내가-설정했던-redirect주소가-된다">Controller : 여기서 endpoint는 내가 설정했던 redirect주소가 된다.</h3>
<pre><code class="language-java"> @GetMapping(&quot;/login/kakao/oauth2&quot;)
  public SingleResult&lt;Object&gt; redirectKakaoLogin(@RequestParam(value = &quot;code&quot;)String code) {
    return responseService.getSingleResult(accountService.getKakaoTokenWithInfo(code));
  }</code></pre>
<h3 id="service--가입한-유저가-있으면-로그인-아니면-회원정보를-만들어-내보낸다">Service : 가입한 유저가 있으면 로그인 아니면 회원정보를 만들어 내보낸다.</h3>
<pre><code class="language-java">
public Object getKakaoTokenWithInfo(String code) {
    String userId = SocialType.K.getType() +&quot;_&quot; + getKakaoInfo(code).getId();
    Users users = userRepository.findByLoginId(userId).orElse(null);
    if(users == null){
      return SocialInfoRes.newInstance(userId,socialRandomPassword(userId),SocialType.K);
    }
    return createToken(users);
  }
    private KTokenInfoRes getKakaoInfo(String code) {
    return kakaoInfoFeignClient
      .getInfo(
        kakaoLoginFeignClient
          .getToken(
            KLoginTokenReq.newInstance(kakaoInfo, code).toString())
          .getAccess_token());
  }
    private String socialRandomPassword(String userId) {
      String systemMil = String.valueOf(System.currentTimeMillis());
      return passwordEncoder.encode(userId + systemMil);
  }

private LoginRes createToken(Users user) {
    return LoginRes.of(jwtProvider.createAccessToken(user.getLoginId(), user.getGroup().getFuncList()), jwtProvider.createRefreshToken(user.getLoginId()));
  }
</code></pre>
<h3 id="socialinfores--소셜로그인-후-최종적으로-client에게-보여질-data-객체이다">SocialInfoRes : 소셜로그인 후 최종적으로 client에게 보여질 data 객체이다.</h3>
<pre><code class="language-java">
package com.send.apiauth.domain.auth.res;

import com.send.moduledomain.domain.user.entity.SocialType;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SocialInfoRes {

  private String userId;
  private String password;
  private String type;
  private String name;

  public static SocialInfoRes newInstance(String userId, String password, SocialType socialType){
    return SocialInfoRes.builder()
      .type(socialType.getType())
      .password(password)
      .userId(userId)
      .build();
  }
}
</code></pre>
<h1 id="결과">결과</h1>
<blockquote>
<p>FeignClient로 소셜로그인을 구현해 보았다. OAuth2를 의존하지 않게 됨으로써 모듈의 분리, 재결합 등을 수월하게 할 수 있고, 코드의 재사용성 또한 좋아진다. 지금 구현한 코드는 어디서나 다시 재활용 하여도 의존하지 않기때문에 몇 가지 데이터만 있으면 다른프로젝트에 이식할 수 있는 환경이 되었다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring) QR-Code Login을 구현해보자]]></title>
            <link>https://velog.io/@ch_kang/Spring-QR-Code-Login%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@ch_kang/Spring-QR-Code-Login%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 05 Nov 2022 08:20:51 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<blockquote>
<p>Telegram 과 같은 채팅어플을 개발중에 QR-LOGIN을 구현하는 기획이 있었다.</p>
</blockquote>
<h2 id="방법-찾기">방법 찾기</h2>
<blockquote>
<p>나는 NAVER에서 구현한 QR-LOGIN이 어떤방식으로 로그인처리를 하는지 알고싶어서 파헤치기 시작했다.</p>
</blockquote>
<h3 id="1-네이버는-qr에-3분의-시간-제한을-두었다">1. 네이버는 QR에 3분의 시간 제한을 두었다.</h3>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/f022f225-d3ae-40ed-9fa5-213ac8dcd572/image.png" alt=""></p>
<h3 id="2-qr을-스캔했을때-접속하는-주소이다-qr-code마다-sessionid를-새로-생성한다">2. QR을 스캔했을때 접속하는 주소이다. QR-code마다 sessionID를 새로 생성한다.</h3>
<pre><code class="language-java">https://nid.naver.com/nidlogin.qrcode?mode=qrcode&amp;qrcodesession=sdHkwNo6NLt958rSgTAwwvVsR8QKeH0Y</code></pre>
<h3 id="3-qr코드-로그인-시에-event-stream을-연결한다는-걸-알-수있다">3. QR코드 로그인 시에 Event-Stream을 연결한다는 걸 알 수있다.</h3>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/eb5780fa-c319-4b1d-9c01-65e20a358c0f/image.png" alt=""></p>
<ul>
<li>연결된 Event Stream 으로 ping정보를 계속 보내 연결되었는지 확인하는 것을 알수 있다.</li>
<li>사진에 나오진 않지만 QR-Code Scan 을 하게되면 인가된 로그인 정보가 서버로 발송되는 구조인것 같다. 발송된 Token이 지금의 통로를 통해 Token이 오게 된다.</li>
</ul>
<h2 id="구상">구상</h2>
<blockquote>
<p>앞서 확인한 NAVER 를 참고하여 그림으로 구상해보자</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/54d3922e-77c2-4754-8837-f47cbc273071/image.png" alt=""></p>
<ol>
<li>QR-CODE LOGIN 페이지 접속</li>
<li>임의의 SessionID 생성 (3분의 시간제한을 가지고 있다.)</li>
<li>생성한 SessionID를 가지고 서버로 SSE 통신 <code>Request</code> </li>
<li>서버에서 SessionID를 키값으로 가진 SSE 통로생성 후 메세지 <code>Response</code></li>
<li>인가된 회원의 앱으로 QR스캔</li>
<li>QR-code에 포함된 주소로 Token 정보 <code>Request</code></li>
<li>서버에서 인가된 토큰 확인 후 연결되어있는 SSE 통로로 Token <code>Response</code></li>
</ol>
<h2 id="코드-작성">코드 작성</h2>
<blockquote>
<p>구상은 끝났으니 코드로 작성해보자. 우선, 우리 프로젝트는 Spirng MVC 이라는 점을 감안했다. 
또 , 데이터의 구독과 발행은 Redis의 pub/sub을 사용하여 여러 인스턴스에서 사용할 수 있게 하였다.</p>
</blockquote>
<h3 id="sse-통신은-비동기-통신이다-mvc에서는-sseemitter-클래스를-통해-구현가능하다">SSE 통신은 비동기 통신이다. MVC에서는 SseEmitter 클래스를 통해 구현가능하다.</h3>
<h4 id="gradlebuild">gradle.build</h4>
<ul>
<li>레디스의 PUB/SUB과 db를 활용 할 예정이기 때문에 레디스를 impl해 준다.<pre><code class="language-groovy"> implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code></pre>
</li>
</ul>
<h4 id="controler">CONTROLER</h4>
<pre><code class="language-java">// SSE 통신 요청 
  @GetMapping(value = &quot;/qrcode-req&quot;)
  @ResponseStatus(HttpStatus.OK)
  public SseEmitter qrcodeReq(@RequestParam String sessionId) throws IOException {
    return sseQrcodeService.newSseEmitterForRedisChannel(sessionId);
  }
 // Ping 체크
  @GetMapping(&quot;/qrcode-req/ping&quot;)
  @ResponseStatus(HttpStatus.OK)
  public CommonResult pingCheck(@RequestParam String sessionId) throws IOException {
    sseQrcodeService.pingCheck(sessionId);
    return responseService.getSuccessResult();
  }
 // QR-Code 주소 -&gt; login token을 응답받을 주소이다.
  @GetMapping(&quot;/qrcode-res&quot;)
  @ResponseStatus(HttpStatus.OK)
  public CommonResult qrcodeRes(@RequestParam String sessionId,
                          @RequestHeader(value = &quot;Authorization&quot;, required = false, defaultValue = &quot;&quot;) String token){
    sseQrcodeService.sendTokenToSseEmitter(sessionId,token.substring(7));
    return responseService.getSuccessResult();
  }</code></pre>
<h4 id="service">SERVICE</h4>
<pre><code class="language-java">package com.send.apiauth.domain.auth.service;

import com.send.apiauth.domain.auth.res.LoginRes;
import com.send.modulecore.exception.CustomRuntimeException;
import com.send.modulecore.response.code.UsersResponseCode;
import com.send.modulesystem.redis.RedisPubService;
import com.send.modulesystem.redis.RedisService;
import com.send.modulesystem.security.jwt.JwtProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

import static java.util.Objects.isNull;

@Service
@RequiredArgsConstructor
@Slf4j
public class SseQrcodeService {

  private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 10; 
  // SSE 만료시간
  private static final Long EXPIRED_TOKEN_TIMEOUT = 185L;
  // 토큰 만료 시간
  private final JwtProvider jwtProvider;
  // jwt 토큰 관리자
  private final RedisService redisService;
  // 레디스 key value 
  private final RedisMessageListenerContainer redisContainer;
  // 레디스 listener container
  private final RedisPubService redisPubService;
  // 레디스 발행 서비스

  /**
  * 최초 SSE 채널을 생성하고 Redis listner Container에 이벤트를 등록한다.
  */
  public SseEmitter newSseEmitterForRedisChannel(final String sessionId) {
    final SseEmitter emitter = new   SseEmitter(TimeUnit.MINUTES.toMillis(DEFAULT_TIMEOUT)); 
    // SSE 통신을 위한 SSe emitter 객체를 생성해 준다.
    MessageListenerAdapter listenerAdapter = new MessageListenerAdapter(setMessageListener(sessionId, emitter));
    // 이벤트가 들어오면 작동할 함수를 정의한다.

    redisContainer.addMessageListener(listenerAdapter, new PatternTopic(getChannelNameWithPrefix(sessionId)));
    // 레디스 컨테이너에 topic과 함수를 등록한다.

    redisService.setValues(getChannelNameWithPrefix(sessionId),now(),Duration.ofSeconds(EXPIRED_TOKEN_TIMEOUT));
    // 레디스에 sessionID를 key로 갖고, 3분의 만료시간을 갖는 데이터를 생성한다.

    log.info(&quot;Added emitter {} from listenerAdapter {}&quot;, emitter, listenerAdapter);

    sendToClient(emitter, &quot;CONNECT&quot;, &quot;CONNECT_&quot; + sessionId, &quot;SUCCESS_&quot; + sessionId + now());
    // 연결된 통로를 통해 연결 성공 데이터를 보낸다.

    emitter.onCompletion(() -&gt; { // 연결이 종로 되었을 때 이벤트를 설정한다.
      log.info(&quot;Removed emitter {} from listenerAdapter {}&quot;, emitter, listenerAdapter);
      redisContainer.removeMessageListener(listenerAdapter);
      // 등록했던 이벤트 리스너를 삭제한다.
      redisService.deleteValues(getChannelNameWithPrefix(sessionId));
      // sessionId를 키로 갖는 데이터를 레디스에서 삭제한다.
    });

    return emitter;
  }

 /**
 * client가 sseEmiter를 종료했는지 확인하기 위해 핑을 체크한다.
 */
  public void pingCheck(String sessionId) {
    sendMessage(sessionId,&quot;ping&quot;);
  }
  /**
  * Client가 QR코드로 인증을 시도한 경우 Token을 보낸다.
  */
  public void sendTokenToSseEmitter(String sessionId, String substring) {
    sendMessage(sessionId,substring);
  }

/**
* 토큰 안에서 정보를 가져와서 다시 토큰을 발행해주는 함수
*/
  private LoginRes createToken(String replaceMessage) {
    String accountId = jwtProvider.getAccountId(replaceMessage);
    List&lt;String&gt; roles = jwtProvider.getRoles(replaceMessage);
    String accessToken = jwtProvider.createAccessToken(accountId,roles);
    String refreshToken = jwtProvider.createRefreshToken(accountId);
    return LoginRes.builder().accessToken(accessToken).refreshToken(refreshToken).build();
  }

/**
* 이벤트를 정의하는 함수
*/
  private MessageListener setMessageListener(String sessionId, SseEmitter emitter) {
    return (message, pattern) -&gt; {
      String replaceMessage = message.toString().replaceAll(&quot;\&quot;&quot;, &quot;&quot;);
      // 메세지가 /ping 이런식으로 오기 때문에 replaceAll로 데이터를 변경해 준다.
      if (replaceMessage.equals(&quot;ping&quot;)) {
      //  ping 인 경우 
        sendPingMessage(sessionId, emitter);
        // ping 메세지를 전달하고
        return;
        // 리턴한다.
      }
      // 그 외 (JWT TOKEN인 경우)
      sendToClient(emitter, &quot;COMPLETE&quot;, &quot;ping_&quot; + sessionId, createToken(replaceMessage)); 
      // 성공했다는 메세지와 함께 새로 만든 토큰을 보낸다.
      log.debug(&quot;Received {} on {}&quot;, message, Thread.currentThread().getName());
    };
  }

/**
* 10초 간격으로 반복해서 PING을 보내는 함수
*/
  private void sendPingMessage(String sessionId, SseEmitter emitter) {
    Timer timer = new Timer();

    TimerTask task = new TimerTask() {
      @Override
      public void run() {
        if(!hasKey(sessionId)) {
        // Redis에  sessionId 키를 조회하여 없으면 
          timer.cancel();
          // 반복을 종료시키고
          sendToClient(emitter, &quot;FAIL&quot;, &quot;FAIL_&quot; + sessionId, &quot;FAIL_&quot; + sessionId + &quot;_&quot; + now());
          // 종료 메세지를 클라이언트로 보낸다.
          emitter.complete();
          // Sse Emitter도 종료 시킨다.
        }else{ // 키가 있다면
          String eventKey = sessionId + &quot;_&quot; + System.currentTimeMillis();
          // 메세지를 만들어
          if(!sendToClient(emitter, &quot;ping&quot;, &quot;ping_&quot; + eventKey, now())) 
          // 핑 메세지를 보낸후 성공 여부를 리턴받는다.
          timer.cancel();
          //실패한경우 타이머를 종료 시킨다.
        }
      }
    };
    timer.schedule(task,0L,10000);

  }

/**
* 메세지를 레디스에 발행하는 함수다.
*/
  private void sendMessage(String sessionId, String message) {
    if (hasKey(sessionId)) {
     // 레디스에 키가 있는지 먼저 확인 후 redisPubService.publicMessageToRedisChannel(getChannelNameWithPrefix(sessionId), message);
     // sessionId 토픽으로 데이터를 발행한다.
    } else {
      keyNotFoundException();
      // 없으면 오루를 발생시킨다.
    }
  }
 /**
 * 레디스에 키가 있는지 조회하는 함수다.
 */ 
  private boolean hasKey(String sessionId) {
    return !isNull(redisService.getValues(getChannelNameWithPrefix(sessionId)));
  }
/**
* 오류를 담당하는 함수
*/
  private void keyNotFoundException(){
    throw new CustomRuntimeException(&quot;세션이 만료되었습니다.&quot;, UsersResponseCode.TOKEN_NOT_FOUND);
  }
/**
* SSE 통신에 데이터를 보내는 함수다.
*/
  private boolean sendToClient(SseEmitter emitter, String name, String id, Object data) {
    try {
      emitter.send(SseEmitter.event()
        .id(id)
        .name(name)
        // 클라이언트에서 메세지를 구분할 때 사용한다.
        .data(data));
     // 파라미터 값으로 받은 데이터들로 이벤트를 만들어 데이터를 보낸다.  
    } catch (IOException | IllegalStateException exception) {
    // 에러발생시
      emitter.completeWithError(exception);
      // 통로를 닫고 에러를 발생시킨다.
      log.info(&quot;연결이 종료되었습니다. 연결 및 데이터를 삭제합니다.&quot;);
      return false;
    }
    return true;
  }

  private static String now() {
    return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
  }
  private String getChannelNameWithPrefix(String channelName) {
    return &quot;qrcode:&quot; + channelName;
  }
}</code></pre>
<h4 id="repsitory">Repsitory</h4>
<h4 id="redis-pubsubconfig">redis pubsubConfig</h4>
<pre><code class="language-java">package com.send.modulesystem.redis;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.util.StringUtils;

import java.util.concurrent.Executors;
/**
* 레디스 구독 발행 설정 클래스이다.
*/
@Configuration
public class RedisPubSubConfig {

  @Value(&quot;${spring.redis.host}&quot;)
  private String redisHost;

  @Value(&quot;${spring.redis.port}&quot;)
  private int redisPort;

  @Value(&quot;${spring.redis.password}&quot;)
  private String password;

  @Bean
  public RedisConnectionFactory redisConnectionFactory() {
    RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
    redisStandaloneConfiguration.setHostName(redisHost);
    redisStandaloneConfiguration.setPassword(password);
    redisStandaloneConfiguration.setPort(redisPort);
    return new LettuceConnectionFactory(redisStandaloneConfiguration);
  }

  @Bean
  public RedisTemplate&lt;String, Object&gt; redisTemplate() {
    final RedisTemplate&lt;String, Object&gt; template = new RedisTemplate&lt;String, Object&gt;();
    template.setConnectionFactory(redisConnectionFactory());
    template.setValueSerializer(new Jackson2JsonRedisSerializer&lt;Object&gt;(Object.class));
    return template;
  }

  @Bean
  RedisMessageListenerContainer redisContainer() {
    final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(redisConnectionFactory());
    container.setTaskExecutor(Executors.newFixedThreadPool(5));
    return container;
  }


}
</code></pre>
<h4 id="redis-pubservice">redis PubService</h4>
<pre><code class="language-java">
package com.send.modulesystem.redis;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service

/**
* 토픽으로 데이터를 발행하는 클래스이다.
*/
public class RedisPubService {

  private final RedisTemplate&lt;String, Object&gt; redisTemplate;

  public boolean publicMessageToRedisChannel(String channelName, String message) {
    redisTemplate.convertAndSend(channelName,message);
    return true;
  }


}
</code></pre>
<h4 id="redis-service">redis Service</h4>
<pre><code class="language-java">package com.send.modulesystem.redis;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.time.Duration;
/**
* 레디스 디비를 담당하는 클래스이다. key / value 값을 저장할때 필요하다.
*/
@Service
@RequiredArgsConstructor
public class RedisService {
    private final RedisTemplate&lt;String, String&gt; redisTemplate;

    public void setValues(String key, String data) {
        ValueOperations&lt;String, String&gt; values = redisTemplate.opsForValue();
        values.set(key, data);
    }

    public void setValues(String key, String data, Duration duration) {
        ValueOperations&lt;String, String&gt; values = redisTemplate.opsForValue();
        values.set(key, data, duration);
    }

    public String getValues(String key) {
        ValueOperations&lt;String, String&gt; values = redisTemplate.opsForValue();
        return values.get(key);
    }

    public void deleteValues(String key) {
        redisTemplate.delete(key);
    }

}
</code></pre>
<h2 id="client">Client</h2>
<h3 id="--임시로-만든-sse-emitter-test용-html-스크립트-이다">- 임시로 만든 SSE EMITTER TEST용 HTML 스크립트 이다.</h3>
<pre><code class="language-html">&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;button id=&quot;reset&quot; onclick=&quot;resetting()&quot;&gt;초기화&lt;/button&gt;

&lt;script&gt;

  &lt;-- SESSION ID 를 생성해주는 함수다. --&gt;
    const generateRandomString = (num) =&gt; {
        const characters = &#39;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&#39;;
        let result = &#39;&#39;;
        const charactersLength = characters.length;
        for (let i = 0; i &lt; num; i++) {
            result += characters.charAt(Math.floor(Math.random() * charactersLength));
        }
        return result;
    }

    let randomChar = generateRandomString(10)// sessionId 생성
    console.log(randomChar)
    let eventSource = new EventSource(`http://localhost:80/api/v1/qrcode-req?sessionId=${randomChar}`)
    //만든 sessionId 로 최초 요청을 보낸다.    

    &lt;!-- 서버에서 보낸 SSE message의 name으로 각각 함수를 설정해 주었다.  --&gt;

    &lt;!--CONNECT 인경우--&gt;
    eventSource.addEventListener(&#39;CONNECT&#39;,(e) =&gt; {
      console.log(e.data)
     //데이터 출력
      fetch(`http://localhost:80/api/v1/qrcode-req/ping?sessionId=${randomChar}`)
    .then(
              (response) =&gt; console.log(response)
      )
    //핑을 체크하는 엔드포인트를 호출한다.
    })

    &lt;!--ping 인 경우--&gt;
    eventSource.addEventListener(&#39;ping&#39;,(e) =&gt; {
      console.log(e.data)
     //데이터를 출력한다
    })

    &lt;!--FAIL인 경우--&gt;
    eventSource.addEventListener(&#39;FAIL&#39;,(e) =&gt; {
        console.log(e.data)
        eventSource.close();
    //SSE 통신을 종료시킨다.
    })

    &lt;!--COMPLETE 인 경우--&gt;
    eventSource.addEventListener(&#39;COMPLETE&#39;,(e) =&gt; {
      console.log(e.data)
     //TODO: 토큰으로 로그인하는 로직이 필요하다.
      eventSource.close();
    //SSE 통신을 종료시킨다.
    })


    &lt;!--초기화 시켰을경우--&gt;
    function resetting() {
        console.log(&quot;close&quot;);
        eventSource.close();
        randomChar = generateRandomString(10)
        eventSource = new EventSource(`http://localhost:80/api/v1/qrcode-req?sessionId=${randomChar}`)

      eventSource.addEventListener(&#39;CONNECT&#39;,(e) =&gt; {
        console.log(e.data)
        fetch(`http://localhost:80/api/v1/qrcode-req/ping?sessionId=${randomChar}`).then(
                (response) =&gt; console.log(response)
        )
      })
      eventSource.addEventListener(&#39;ping&#39;,(e) =&gt; {
        console.log(e.data)
      })
        eventSource.addEventListener(&#39;FAIL&#39;,(e) =&gt; {
            console.log(e.data)
            eventSource.close();
        })
      eventSource.addEventListener(&#39;COMPLETE&#39;,(e) =&gt; {
        console.log(e.data)
        eventSource.close();
        // 토큰을 받고 홈페이지로 리다이렉트
      })
    }


&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring) Docker JDK 무조건 신뢰 금지]]></title>
            <link>https://velog.io/@ch_kang/Spring-Docker-JDK-%EB%AC%B4%EC%A1%B0%EA%B1%B4-%EC%8B%A0%EB%A2%B0-%EA%B8%88%EC%A7%80</link>
            <guid>https://velog.io/@ch_kang/Spring-Docker-JDK-%EB%AC%B4%EC%A1%B0%EA%B1%B4-%EC%8B%A0%EB%A2%B0-%EA%B8%88%EC%A7%80</guid>
            <pubDate>Tue, 30 Aug 2022 00:47:42 GMT</pubDate>
            <description><![CDATA[<h3 id="문제">문제</h3>
<ul>
<li>Telegram message 일일근태 API 가 Local에서는 작동하나, ec2에서는 작동안하는 문제 확인.</li>
</ul>
<h3 id="원인-찾기">원인 찾기</h3>
<pre><code class="language-ruby">FROM --platform=linux/amd64 openjdk:17-slim

ARG JAR_FILE=build/libs/*.jar


COPY ${JAR_FILE} /app/isolution-api.jar

WORKDIR /app

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;-Dspring.profiles.active=${PROFILE}&quot;, &quot;-Dcom.amazonaws.sdk.disableEc2Metadata=true&quot;, &quot;/app/isolution-api.jar&quot;]</code></pre>
<ul>
<li>프로젝트에 있던  도커파일을 빌드하여 로컬에서 도커를 띄워본다.</li>
<li>Docker Log를 띄우니 <em>unsatisfiedlinkerror /usr/local/openjdk-11/lib/libfontmanager.so</em> 가 발생
<img src="https://velog.velcdn.com/images/ch_kang/post/4f85a938-4526-46ec-b649-9b032ce51e90/image.png" alt=""></li>
<li>Library 경로 오류로 확인되었다.</li>
</ul>
<h3 id="삽질-1">삽질 1</h3>
<ul>
<li>해당 경로에 libfontmanager.so 파일을 넣어주는 방법을 생각했으나</li>
<li>so 파일은 linux 에서만 확인되는 파일임을 확인하고 포기</li>
</ul>
<h3 id="삽질-2">삽질 2</h3>
<ul>
<li>libfontmanager.so 파일에 대해 찾던 결과 mac에서는 dylib를 사용하며,
해당 확장자는 동적라이브러리 즉, 사용될때 로드되는 라이브러리.</li>
<li>html을 이미지로 변환하는 과정중에  java 7에서 사용되었던 라이브러리가 있었고 해당 부분에서 문제가 발생되었음을 확인.</li>
</ul>
<h3 id="문제해결">문제해결</h3>
<blockquote>
<p><a href="https://github.com/docker-library/openjdk/issues/335">stackoverflow</a> #335 참고</p>
</blockquote>
<ul>
<li><p>Docker에서 jdk 일부버전 (slim... 등등) 은 동적라이브러리를 포함하지 않는다.</p>
<pre><code class="language-ruby">bzip2/stable,now 1.0.6-8.1 amd64 [installed]
fontconfig-config/stable,now 2.11.0-6.7 all [installed,automatic]
java-common/stable,now 0.58+deb9u1 all [installed,automatic]
libavahi-client3/stable,now 0.6.32-2 amd64 [installed,automatic]
libavahi-common-data/stable,now 0.6.32-2 amd64 [installed,automatic]
libavahi-common3/stable,now 0.6.32-2 amd64 [installed,automatic]
libbsd0/stable,now 0.8.3-1 amd64 [installed,automatic]
libcups2/stable,now 2.2.1-8+deb9u3 amd64 [installed,automatic]
libdbus-1-3/stable,now 1.10.26-0+deb9u1 amd64 [installed,automatic]
libfontconfig1/stable,now 2.11.0-6.7+b1 amd64 [installed,automatic]
libfreetype6/stable,now 2.6.3-3.2 amd64 [installed]
libgmp10/stable,now 2:6.1.2+dfsg-1 amd64 [installed,automatic]
libgnutls30/stable,now 3.5.8-5+deb9u4 amd64 [installed,automatic]
libgssapi-krb5-2/stable,now 1.15-1+deb9u1 amd64 [installed,automatic]
libhogweed4/stable,now 3.3-1+b2 amd64 [installed,automatic]
libidn11/stable,now 1.33-1 amd64 [installed,automatic]
libjpeg62-turbo/stable,now 1:1.5.1-2 amd64 [installed,automatic]
libk5crypto3/stable,now 1.15-1+deb9u1 amd64 [installed,automatic]
libkeyutils1/stable,now 1.5.9-9 amd64 [installed,automatic]
libkrb5-3/stable,now 1.15-1+deb9u1 amd64 [installed,automatic]
libkrb5support0/stable,now 1.15-1+deb9u1 amd64 [installed,automatic]
liblcms2-2/stable,stable,now 2.8-4+deb9u1 amd64 [installed,automatic]
libnettle6/stable,now 3.3-1+b2 amd64 [installed,automatic]
libnspr4/stable,now 2:4.12-6 amd64 [installed,automatic]
libnss3/stable,stable,now 2:3.26.2-1.1+deb9u1 amd64 [installed,automatic]
libpcsclite1/stable,now 1.8.20-1 amd64 [installed,automatic]
libpng16-16/stable,now 1.6.28-1+deb9u1 amd64 [installed,automatic]
libsqlite3-0/stable,now 3.16.2-5+deb9u1 amd64 [installed,automatic]
libx11-6/stable,now 2:1.6.4-3+deb9u1 amd64 [installed,automatic]
libx11-data/stable,now 2:1.6.4-3+deb9u1 all [installed,automatic]
libxau6/stable,now 1:1.0.8-1 amd64 [installed,automatic]
libxcb1/stable,now 1.12-1 amd64 [installed,automatic]
libxdmcp6/stable,now 1:1.1.2-3 amd64 [installed,automatic]
libxext6/stable,now 2:1.3.3-1+b2 amd64 [installed,automatic]
libxi6/stable,now 2:1.7.9-1 amd64 [installed,automatic]
libxrender1/stable,now 1:0.9.10-1 amd64 [installed,automatic]
libxtst6/stable,now 2:1.2.3-1 amd64 [installed,automatic]
openjdk-8-jdk-headless/stable,stable,now 8u212-b01-1~deb9u1 amd64 [installed]
openjdk-8-jre-headless/stable,stable,now 8u212-b01-1~deb9u1 amd64 [installed,automatic]
ucf/stable,now 3.0036 all [installed,automatic]
unzip/stable,now 6.0-21+deb9u1 amd64 [installed]
x11-common/stable,now 1:7.7+19 all [installed,automatic]
xz-utils/stable,now 5.2.2-1.2+b1 amd64 [installed]</code></pre>
</li>
<li><p>따라서 Dockerfile중 jdk를 변경</p>
</li>
</ul>
<blockquote>
<p>openjdk:17-slim -&gt; openjdk:17-ea-33-jdk-buster</p>
</blockquote>
<ul>
<li>도커에서 작동됨을 확인하고 문제 해결</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter ) 이젠 dart다. 자바 개발자가 이해하는 dart]]></title>
            <link>https://velog.io/@ch_kang/Flutter-%EC%9D%B4%EC%A0%A0-dart%EB%8B%A4.-%EC%9E%90%EB%B0%94-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-dart</link>
            <guid>https://velog.io/@ch_kang/Flutter-%EC%9D%B4%EC%A0%A0-dart%EB%8B%A4.-%EC%9E%90%EB%B0%94-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-dart</guid>
            <pubDate>Fri, 26 Aug 2022 12:19:24 GMT</pubDate>
            <description><![CDATA[<h3 id="배우게-된-계기">배우게 된 계기</h3>
<ul>
<li>예전부터 배우고 싶은 언어 중 하나였다. </li>
<li>그러다 이번에 채팅 어플을 개발한다는 소식이 들려와 본격적으로 어플을 만들어 보려고한다.</li>
<li>너무 잡탕밥이 되어가는 개발인생인거 같지만 언젠가 쓸모있지 않겠는가...</li>
</ul>
<h3 id="본론">본론</h3>
<ul>
<li>자바언어랑 흡사한 부분이 많아 다른부분만 설명할 것이다.</li>
</ul>
<h3 id="생성자-선택적-매개변수-사용">생성자 [선택적 매개변수 사용]</h3>
<pre><code class="language-dart">  Rectangle({this.origin = const Point(0, 0), this.width = 0, this.height = 0});</code></pre>
<ul>
<li>{  } 다음과 같이 자바 예에 있는 4개의 생성자를 모두 대체하는 하나의 생성자이다. </li>
<li>dart에는 Over loading이 없다.</li>
<li>dart는 null check를 한다. nullable한 타입이 아니라면 requred를 붙이거나 초기값을 넣어 줘야한다.</li>
<li>다음과 같이 사용시 nameed param으로 사용할 수 있다.</li>
</ul>
<h3 id="읽기전용-변수추가-read-only--private-접근제한자">읽기전용 변수추가 (Read Only) = private 접근제한자</h3>
<pre><code class="language-dart">int _speed = 0;
</code></pre>
<ul>
<li>_를 추가하여 읽기 전용변수를 만들 수 있다.</li>
</ul>
<h3 id="팩토리-만들기">팩토리 만들기</h3>
<ul>
<li>short Description: 팩토리란 자바 핵심 패턴 중 하나인 singleTon Pattern을 따른다.</li>
<li><blockquote>
<p> <a href="https://sudarlife.tistory.com/entry/%ED%94%8C%EB%9F%AC%ED%84%B0%EB%8B%A4%ED%8A%B8-%ED%8C%A9%ED%86%A0%EB%A6%ACFactory-%ED%8C%A8%ED%84%B4-%EC%98%88%EC%A0%9C%EB%A1%9C-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0">자세히 알아보기</a></p>
</blockquote>
</li>
</ul>
<pre><code>abstract class Pizza {
  late String orderNumber;
  double getPrice();
  factory Pizza.fromJson(Map&lt;String,dynamic&gt; json) {
    switch (json[&#39;type&#39;] as PizzaType) {
      case PizzaType.HamMushroom:
        return HamAndMushroomPizza(json[&#39;orderNumber’]);
      case PizzaType.Deluxe:
        return DeluxePizza(json[&#39;orderNumber’]);
      case PizzaType.Seafood:
        return SeafoodPizza(json[&#39;orderNumber’]);
    }
  }
}</code></pre><ul>
<li>예제는 친근한 피자로 들겠다.</li>
<li>java에서 static 생성자로 하나의 인스턴스를 돌려쓰는것이랑 같다.<h3 id="dynamic--object--any">dynamic = Object = any</h3>
</li>
<li>위 코드에서 보인다. 어떤 형이던 가질수 있는 형태이다.</li>
</ul>
<h3 id="const--final-차이점">const &amp; final 차이점</h3>
<ul>
<li>const -&gt; compile 때 할당 -&gt; 코드를 번역할때</li>
<li>final -&gt; runtime 때 할당 -&gt; 코드를 읽을때</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring) Immutable을 경험하다.]]></title>
            <link>https://velog.io/@ch_kang/Spring-Immutable%EC%9D%84-%EA%B2%BD%ED%97%98%ED%95%98%EB%8B%A4</link>
            <guid>https://velog.io/@ch_kang/Spring-Immutable%EC%9D%84-%EA%B2%BD%ED%97%98%ED%95%98%EB%8B%A4</guid>
            <pubDate>Fri, 26 Aug 2022 06:18:25 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ch_kang/post/a06fde24-57ab-45ea-a8f4-a01976d53d03/image.png" alt=""></p>
<h2 id="상황">상황</h2>
<ul>
<li>위와같이 일일근태를 간결히 보여주는 테이블을 만들고 있었다.</li>
</ul>
<h2 id="문제">문제</h2>
<ul>
<li>union으로 간단히 해결될 거라 생각했던것과 달리 데이터가 생각대로 나오지 않았다.</li>
<li>결국 DB에서는 union으로 해당 되는 데이터를 다 끌고와서 비즈니스 로직에서 처리하기로 하였다.</li>
<li>처리하는 과정 중  UnsupportedOperationException : A TupleBackedMap cannot be modified.
<img src="https://velog.velcdn.com/images/ch_kang/post/f8f2082f-a5f9-463d-8e61-584cfd0e1c1f/image.png" alt="">
위 같은 에러가 발생하였다.</li>
</ul>
<h2 id="원인-찾기">원인 찾기</h2>
<ul>
<li><p>해당오류를 검색해보아도 중국인 사이트 밖에 나오지 않았다.
번역의 힘을 빌린 결과 List로 생성시 불변리스트로 생성되기 때문에 ArrayList를 새로 생성하라는 내용으로 확인하였다.</p>
</li>
<li><p>위 결과를 바탕으로 변경가능한 arrayList를 새로 생성한 후 넣어주는 방식으로 진행 하였으나 실패</p>
</li>
<li><p>다시 원점으로 돌아가 무엇이 문제인지 삽질의 결과 List가 아닌 리스트 안에 있는 객체가 불변객체였다.</p>
</li>
<li><p>jpa영속성 관련 인줄 알고 또 헛다리 짚을뻔 했으나 해당 원인임을 확인하였다.</p>
</li>
</ul>
<h2 id="해결">해결</h2>
<ul>
<li>원인을 알았으니 해결은 금방이다. 깊은 복사를 통해 객체를 새로 할당해 주었다.</li>
<li>성능상 이슈가 있을 수 있지만 하루 한번 배치작업하는 정도로 사용되는 Api였기 때문에 크게 신경 쓰지 않았다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring) Telegram message에 Table 을 보내보자 ]]></title>
            <link>https://velog.io/@ch_kang/Spring-Telegram-message%EC%97%90-Table-%EC%9D%84-%EB%B3%B4%EB%82%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@ch_kang/Spring-Telegram-message%EC%97%90-Table-%EC%9D%84-%EB%B3%B4%EB%82%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 25 Aug 2022 07:30:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>문제 : Telegram message API에는 markdown과 html을 사용할 수 있으나, table을 사용할 수 없었다.</p>
</blockquote>
<h3 id="첫번째-시도">첫번째 시도.</h3>
<p>처음엔 스택오버플로우 형님들이 올려두신 방법대로 문자로 테이블모양을 그렸었다.</p>
<p>그러나, 문자열의 길이가 제각각이면 테이블 모양이 깨지고 별로 이쁘지가 않았기에 Pass</p>
<h3 id="두번째-시도">두번째 시도</h3>
<p>둘째로 Html을 이미지로 변환하는 기능이 있다고 하여 해당 기능을 사용했다.
코드는 아래와 같다</p>
<pre><code class="language-java">package api.isolution.global.util.html;

import javax.swing.*;
import javax.swing.text.Document;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import java.awt.*;
import java.awt.image.BufferedImage;

public class HtmlToImage {


  public static BufferedImage create(String text, int width, int height) {
    BufferedImage image = null;
    JEditorPane pane = new JEditorPane();
    Kit kit = new Kit();
    pane.setEditorKit(kit);
    pane.setEditable(false);
    pane.setMargin(new Insets(0,0,0,0));
    try {
      pane.setText(text);
      image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
      Graphics g = image.createGraphics();
      Container c = new Container();
      SwingUtilities.paintComponent(g, pane, c, 0, 0, width, height);
      g.dispose();
    } catch (Exception e) {
      System.out.println(e);
    }
    return image;
  }

  @SuppressWarnings(&quot;serial&quot;)
  static class Kit extends HTMLEditorKit
  {
    public Document createDefaultDocument() {
      HTMLDocument doc = (HTMLDocument) super.createDefaultDocument();
      doc.setTokenThreshold(Integer.MAX_VALUE);
      doc.setAsynchronousLoadPriority(-1);
      return doc;
    }
  }

}
</code></pre>
<h4 id="htmltoimageservice">HtmlToImageService</h4>
<pre><code class="language-java">package api.isolution.global.util.html;

import api.isolution.global.common.response.SingleResult;
import api.isolution.global.util.img.dto.ImgResponse;
import api.isolution.global.util.img.service.ImgService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

@Service
@RequiredArgsConstructor
@Slf4j
public class HtmlToImageService {

  private final ImgService imgService;


  public String makeImg(String source,int size)  {
    BufferedImage imgSorce;
    imgSorce = HtmlToImage.create(source, 500, size * 30 + 100);
    MultipartFile image = convertBufferedImageToMultipartFile(imgSorce);
    SingleResult&lt;ImgResponse&gt; prod = imgService.uploadFiles(image, &quot;prod&quot;);
    log.info(&quot;complete&quot;);
    return prod.getData().getUrl();
  }


  private MultipartFile convertBufferedImageToMultipartFile(BufferedImage image) {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    try {
      ImageIO.write(image, &quot;jpeg&quot;, out);
    } catch (IOException e) {
      log.error(&quot;IO Error&quot;, e);
      return null;
    }
    byte[] bytes = out.toByteArray();
    return new CustomMultipartFile(bytes, &quot;image&quot;, &quot;image.jpeg&quot;, &quot;jpeg&quot;, bytes.length);
  }
}</code></pre>
<p>프로젝트에 S3로 파일을 전송하는 service가 있었기 때문에</p>
<p>로컬에 저장하지 않고 S3에 파일을 저장하였다.</p>
<p>그 후,</p>
<p>저장시킨 파일의 URL을 받아 message로 보냈다.</p>
<p>이 때, markdown 문법에서 지원해주는 <a href="">이런문법</a>이 있다</p>
<p>이미지 주소를 걸면 이미지가 자동으로 나타난다.</p>
<p>결과 &gt;&gt;&gt;&gt;&gt;&gt;</p>
<p><img src="https://velog.velcdn.com/images/ch_kang/post/225dfd7c-0014-4dc3-9f71-b1c1201c2bb0/image.png" alt=""></p>
<p>보통 테이블 만들때 라이브러리를 사용했었는데</p>
<p>손으로 직접 태그를 작성하려니 힘들었다.</p>
]]></description>
        </item>
    </channel>
</rss>