<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Noah-wilson0.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 16 Dec 2025 11:42:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Noah-wilson0.log</title>
            <url>https://velog.velcdn.com/images/noah-wilson0/profile/008f1fee-f40c-446d-b819-bddc943b02db/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Noah-wilson0.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/noah-wilson0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[kakao mobility api 102/103 code 트러블 슈팅 과정]]></title>
            <link>https://velog.io/@noah-wilson0/kakao-mobility-api-102103-code-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@noah-wilson0/kakao-mobility-api-102103-code-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Tue, 16 Dec 2025 11:42:24 GMT</pubDate>
            <description><![CDATA[<p>Ai 여행 일정 생성 기능 구현중에 생긴 일이다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/bd8c2ac4-e5bb-45d2-867e-a02c01d7598a/image.png" alt=""></p>
<p>위 사진과 같이 출발지/목적지의 도로 정보를 탐색할 수 없을때 kakao mobility api의 자동차 길찾기에서 102/103 result code를응답한다. </p>
<p>이전에 비슷한 문제로  105,106 result code 응답시 요청 파라미터에 roadEvent를 추가 설정해주며 해결할 수 있었지만,</p>
<p>102/103 code의 경우 출발지/목적지의 좌표가 카카오 내부 좌표 데이터와 다르게 너무 큰 차이가 난다거나, 주변에 도로가 전혀 없는 것이 문제라고 판단했다.</p>
<p>우선 다른 장소들도 많지만 이슈가 생긴 &quot;성산일출봉&quot;을 기준으로 이슈를 해결하겠다.</p>
<blockquote>
<p>현재 사용 중인 여행 장소 데이터셋은 한국관광공사 Tour API를 기반으로 하고 있다.</p>
</blockquote>
<p>한국관광공사 Tour API의 성산 일출봉 좌표 값으로 구글맵에 적용해보면 주변 도로와 떨어진 곳에 좌표값이 설정되어 있다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/a2289e92-a20d-4492-96fb-49cd0f725562/image.png" alt=""></p>
<p>이를 해결하기 위해 2가지 해결방법을 떠올렸다.</p>
<ol>
<li><p><a href="https://developers.kakaomobility.com/docs/navi-api/origins/">다중 출발지 길찾기</a>/<a href="https://developers.kakaomobility.com/docs/navi-api/destinations/">다중 목적지 길찾기</a>의 요청 파라미터의 radius를 설정해주어 출발지/목적지의 길찾기 반경을 조절하여 해결한다.</p>
</li>
<li><p>출발지/목적지의 주소 값을 이용하여 지오 코딩을 통해 도로와 출발지/목적지와 비슷한 근처 주차장및 적절한 좌표를 활용하여 해결한다. 지오코딩을 사용할 플랫폼은 카카오를 먼저 사용해본다. 
같은 플랫폼이므로 똑같은 주소라도 좌표값이 카카오 서비스에 문제가 없는 좌표를 사용중 일 것 같다. </p>
</li>
</ol>
<p>결론적으로 1안은 실패했다.
다중 출발지/목적지 길찾기의 radius는 10KM를 지원하지만, 한국관광공사 Tour API의 성산 일출봉 좌표 값이 도로와 상당히 떨어져 있어서 그런지 최대치로 요청해도 결과가 나오지 않았기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/33ddcfd6-d5aa-4a90-b7b2-8e5b0eedf79b/image.png" alt=""></p>
<p>2안을 테스트할 차례이다. 
우선 카카오맵 REST API를 사용하기 위해<a href="https://velog.io/@noah-wilson0/%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A7%B5-%ED%99%9C%EC%84%B1%ED%99%94-%EB%B0%A9%EB%B2%95">선행과정</a>을 해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/a365e068-edf0-47a6-abfa-df38ebb3bcbf/image.png" alt=""></p>
<p>카카오 맵 - 로컬 - REST API-주소로 좌표 변환으로 얻은 좌표를 구글맵에 적용해보면 주변에 도로가 가까이 있다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/72a873b3-f6d8-43f0-9e2e-fcc127c09cb8/image.png" alt=""></p>
<p>이 좌표를 가지고 다시 다중 출발지/목적지 길찾기에 요청을 하면 당연히  200 OK와 정상적인 응답이 나온다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/0b3a5e19-f1df-497b-a5ff-ceee48aa571e/image.png" alt=""></p>
<p>또한 다중 출발지/목적지 길찾기를 쓰지않고 기존에 사용하던 자동차 길찾기 API에 요청해도 정상적으로 응답하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/95b2726e-b0b6-4f89-95f3-588b00d1d7e2/image.png" alt=""></p>
<p> 카카오 플랫폼의 주소 - 좌표 변환 데이터 를 쓰게 되면  카카오의 자동차 길찾기 API가  문제 없이 작동한다. </p>
<p> 나중에 운영환경을 위해서 비용을 고려하여 다른 주소 - 좌표 변환 API들과 비교 및 길찾기 요청을 추가적으로 해보았다.</p>
<p><a href="https://developers.google.com/maps/billing-and-pricing/pricing?_gl=1*1ryf7w*_up*MQ..*_ga*MTY0Njk1NjU3MC4xNzY1ODgyNjU3*_ga_SM8HXJ53K2*czE3NjU4ODI2NTYkbzEkZzAkdDE3NjU4ODI2NTYkajYwJGwwJGgw*_ga_NRWSTWS78N*czE3NjU4ODI2NTYkbzEkZzEkdDE3NjU4ODI2NjEkajU1JGwwJGgw#places-pricing">구글</a>의 경우 무료 사용 10,000건이후 유료이다. 
 <img src="https://velog.velcdn.com/images/noah-wilson0/post/00cdd66c-f332-4577-9c8d-9a173e4d37c7/image.png" alt=""></p>
<p><a href="https://developers.kakao.com/docs/latest/ko/getting-started/quota#monthly">카카오</a>의 경우 일간 100,000건 무료 이므로 카카오를 계속 사용하는 것이 좋아 보인다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/9a450402-a392-4dd9-9ba2-8552cda2e132/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/bba411a5-83c4-4679-baa0-58c0f7a89166/image.png" alt=""></p>
<h2 id="정리">정리</h2>
<p>다중 길찾기 API는 무료 요청 제공량이 상대적으로 적기 때문에, 자동차 길찾기 실패 시 곧바로 다중 길찾기를 호출하기보다는 먼저 지오코딩을 수행한 후 자동차 길찾기를 재시도하는 방식을 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/55617598-c2f9-4893-8fdd-dcc9745f43a3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[카카오 맵 활성화 방법]]></title>
            <link>https://velog.io/@noah-wilson0/%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A7%B5-%ED%99%9C%EC%84%B1%ED%99%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@noah-wilson0/%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A7%B5-%ED%99%9C%EC%84%B1%ED%99%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 16 Dec 2025 10:01:35 GMT</pubDate>
            <description><![CDATA[<p>카카오 맵 로컬 - 주소로 좌표 변환 기능을 사용해보기 위해 카카오 디밸로퍼스에서 설정중에 해결과정이다. </p>
<ol>
<li>카카오 맵을 사용 권한을 활성하기 위해 사용설정을 하려고 했다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/0f1cb60e-0d44-43f4-9312-b5a5202535e7/image.png" alt=""></li>
</ol>
<p>하지만 카카오 맵 사용 권한을 활성화 하기위해 
&quot;추가 기능 신청&quot;을 해야 된다고 뜬다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/49e52ff0-7225-4d79-991b-0b114a0111eb/image.png" alt=""></p>
<p>테스트만 해보고 싶었던 건데 추가 기능 신청까지 해야되나?..</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/cc701d5b-7a85-4080-b4e9-d5da0732f503/image.png" alt=""></p>
<p>그래서 테스트 앱으로 시도해보기 위해 찾아보았다.
<a href="https://developers.kakao.com/docs/latest/ko/app-setting/app#test-app-permission">테스트 앱용 추가기능</a>을 참고하자.</p>
<p>테스트 앱용에서 카카오 맵 사용설정이 바로 활성화 된다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/6cf5d10a-2fbd-4127-86bb-e8ac9a8d7e4e/image.png" alt=""></p>
<p>요청을 보내면 성공하게 된다.
테스트앱의 REST API KEY를 사용하여 카카오 맵 로컬 - 주소로 좌표 변환 요청을 보내면 성공하게 된다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/1e7f73e8-a8d7-48ab-b780-a56e04999aae/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[카카오 디벨로퍼 개인 개발자 비즈앱 전환 방법]]></title>
            <link>https://velog.io/@noah-wilson0/%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%94%94%EB%B2%A8%EB%A1%9C%ED%8D%BC-%EA%B0%9C%EC%9D%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%B9%84%EC%A6%88%EC%95%B1-%EC%A0%84%ED%99%98-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@noah-wilson0/%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%94%94%EB%B2%A8%EB%A1%9C%ED%8D%BC-%EA%B0%9C%EC%9D%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%B9%84%EC%A6%88%EC%95%B1-%EC%A0%84%ED%99%98-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 16 Dec 2025 09:48:48 GMT</pubDate>
            <description><![CDATA[<ol>
<li>앱 아이콘 등록</li>
</ol>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/5df94c60-1c30-4128-92d8-f487fff7d2ea/image.png" alt=""></p>
<ol start="2">
<li><p>개인 개발자 비즈앱-카카오 비즈니스 통합 서비스 약관 동의
<img src="https://velog.velcdn.com/images/noah-wilson0/post/3e9f4e8e-eecf-4df8-8baa-618e082a10b6/image.png" alt=""></p>
</li>
<li><p>개인 개발자 비즈앱 전환버튼 클릭- 전환 목적 작성</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/7513adfc-2bdd-4b7f-a96c-d2c40afcb026/image.png" alt=""></p>
<ol start="4">
<li>결과
<img src="https://velog.velcdn.com/images/noah-wilson0/post/d93c6800-be60-4898-a1b6-20298bdd6657/image.png" alt=""></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[REST API 회원 리소스 리팩토링 정리]]></title>
            <link>https://velog.io/@noah-wilson0/REST-API-%ED%9A%8C%EC%9B%90-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@noah-wilson0/REST-API-%ED%9A%8C%EC%9B%90-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 16 Sep 2025 07:33:07 GMT</pubDate>
            <description><![CDATA[<p>이전 포스팅에서 REST API 설계 원칙을 공부하고 실제 프로젝트의 URI를 리팩토링하는 과정에서,
가장 고민이 많았던 부분은 회원 관련 API였다.</p>
<p>프로젝트 초기에 회원 관련 API는 /auth, /members, /profile 세 가지 리소스로 나누어 사용하고 있었다.</p>
<ul>
<li>/auth → 회원/비회원 여부를 구분하는 용도</li>
<li>/members → 회원 엔터티에 대한 CRUD와 동시에 로그인/로그아웃까지 담당</li>
<li>/profile → 회원 활동 데이터 조회</li>
</ul>
<p>그러나 REST API 설계 원칙을 다시 살펴보니 현재 구조는 문제가 있었다.</p>
<ul>
<li>/members에서 인증(로그인/로그아웃) 기능까지 담당하고 있었음</li>
<li>/profile은 사실상 회원 엔터티(/members/{memberId})의 역할을 중복하고 있었음</li>
<li>/auth는 단순히 회원/비회원을 구분하는 수준에 그치고 있었음</li>
</ul>
<p>즉, 각 리소스의 역할이 뒤섞여 있어서, 실제 개발 과정에서 API의 역할을 직관적으로 파악하기 어려웠다.
결국 코드를 직접 확인하거나 프론트엔드에서 호출되는 API를 추적해야 무슨 역할을 하는지 알 수 있었다.</p>
<h2 id="리팩토링-방향">리팩토링 방향</h2>
<ul>
<li><p>/members
회원 엔터티 자체를 다루는 리소스로 제한.
→ 회원가입(등록) 및 공개 가능한 회원 정보 조회 정도로만 사용.</p>
</li>
<li><p>/auth
인증(Authentication) 전용 리소스.
→ 로그인, 로그아웃, 토큰 갱신 등 인증/인가와 직접 관련된 작업만 담당.</p>
</li>
<li><p>/profile → /me
/profile은 사실상 /members/{memberId}와 동일한 의미로 쓰이고 있었음.
하지만 REST 관례상 현재 로그인한 사용자 자신을 표현하는 /me가 더 직관적이므로 이를 대체 리소스로 사용.
→ 내 정보 조회, 내 활동(여행 일정 등) 조회/수정에 활용.</p>
</li>
</ul>
<h2 id="결론">결론</h2>
<h4 id="members--회원-엔터티-관리">/members — 회원 엔터티 관리</h4>
<p>회원가입과 같은 비회원 등록 작업이나, 회원 목록·프로필 조회 등 공개 정보 제공이 필요한 상황에서 사용된다.</p>
<pre><code class="language-java">POST   /members                  # 회원가입
GET    /members                  # 회원 목록 조회 (예: 랭킹, 검색 결과)
GET    /members/{memberId}       # 특정 회원의 공개 프로필 조회</code></pre>
<h4 id="me--현재-로그인한-사용자">/me — 현재 로그인한 사용자</h4>
<p>항상 현재 로그인한 나 자신을 의미하므로 별도의 식별자를 붙이지 않는다.
내 계정 정보와 내가 만든 활동 데이터를 조회·수정할 수 있다.</p>
<pre><code class="language-java">
- 회원 자기 정보
GET    /me                  # 내 계정 정보
PATCH  /me/nickname         # 닉네임 수정
PATCH  /me/password         # 비밀번호 수정
DELETE /me                  # 회원 탈퇴

- 회원 활동 데이터
GET    /me/travel-plans     # 내가 만든 여행 일정 목록
GET    /me/travel-plans/42  # 내 여행 일정 단일 조회</code></pre>
<h4 id="auth--인증과-인가">/auth — 인증과 인가</h4>
<p>/auth는 말 그대로 로그인, 로그아웃, 토큰 갱신 등 인증 관련 작업만을 담당한다.
회원 데이터나 프로필은 여기서 다루지 않는다.</p>
<pre><code class="language-java">- 로컬 로그인
POST   /auth/sign-in
POST   /auth/sign-out
POST   /auth/refresh         # JWT 재발급

- 로그인 여부 확인
GET    /auth/me              # 현재 로그인 상태 확인</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[REST API 설계 원칙]]></title>
            <link>https://velog.io/@noah-wilson0/REST-API-%EC%84%A4%EA%B3%84-%EC%9B%90%EC%B9%99</link>
            <guid>https://velog.io/@noah-wilson0/REST-API-%EC%84%A4%EA%B3%84-%EC%9B%90%EC%B9%99</guid>
            <pubDate>Sun, 14 Sep 2025 13:57:45 GMT</pubDate>
            <description><![CDATA[<h2 id="rest-api-설계-원칙의-필요성을-느낀-계기">REST API 설계 원칙의 필요성을 느낀 계기</h2>
<p>spring &amp; react를 사용해 MVP(Minimum viable Product) 프로젝트를 만드는 과정중 REST API END POINT를 리팩토링 하는 과정에서 처음 rest pai 설계 원칙을 참고하여 END POINT를 개발한 후 end point를 수정해야 했었다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/980139b2-3c07-46c8-b71f-21c734c6634a/image.png" alt="">
<img src="https://velog.velcdn.com/images/noah-wilson0/post/6b644888-7308-44f8-afe9-fc478811e142/image.png" alt=""></p>
<p>초기에는 빠르게 프로토 타입으로 프로젝트를 완성하는 것을 목표로 했기 때문에,  REST API 설계 원칙을 완전히 이해하지 못한 채로 위 사진과 같이 엉성한 REST API를 구성하였었다.</p>
<p>그러나 리팩토링 시점에 이르러, API 버전 관리, 언더바(_) 사용, 명사의 단수/복수 표현, 리소스-식별자-서브 리소스 계층 구조를 제대로 지키지 않았던 문제들이 
생겨났다.</p>
<p>그 결과, 단순히 End Point만 수정하는 과정임에도 불구하고 &quot;이 API가 어떤 요청을 처리하는지&quot;를 코드까지 열어봐야 이해할 수 있었습니다. 프론트엔드 개발 과정에서도, 특정 화면에 필요한 여러 개의 API 중 어떤 것이 어떤 데이터를 반환하는지 직관적으로 구분하기 어려웠습니다.</p>
<p>이를 통해 REST API 설계 원칙을 정확히 지키는 것의 중요성을 크게 깨닫게 되었고, 블로그를 통해 REST API 설계 원칙을 정리하고, 실제 리팩토링 과정을 기록해 두기로 했습니다.</p>
<h2 id="rest-api--원칙-및-제약">REST API  원칙 및 제약</h2>
<ol>
<li><p>일관된 인터페이스(Uniform Interface)
URI로 지정한 리소스에 대한 조작을 통일하고 한정적인 인터페이스로 수행하여야 한다. 즉, API 설계자가 정한 HTTP METHOD와 규약으로만 수행할 수 있다.</p>
</li>
<li><p>클라이언트-서버(Client — Server)
자원를 제공하는 쪽이 서버, 자원을 요청 하는 쪽을 클라이언트라 한다. 이 둘은 독립적으로 분리되어 구성되어야 한다. 각각 별도로 개발되고 대체 가능하도록 유지 해야 한다.</p>
</li>
<li><p>무상태(Stateless)
작업에 대한 요청의 상태 정보를 저장하거나 관리 하지 않는다. 세션, 인증, 쿠키 등의 정보는 별도로 저장하기 때문에 요청에 대한 어떠한 상태 정보를 저장하지 않고 단순 처리할 수 있어야 한다. 이를 위해 모든 요청에는 필요한 모든 정보가 포함되어야 한다.</p>
</li>
<li><p>캐시 가능(Cacheable)
서버는 캐시 가능 여부를 제공해야 한다. 캐시가 가능 할 경우 클라이언트는 응답 데이터를 재 사용 하여 성능과 서버의 가용성을 올릴 수 있다.</p>
</li>
</ol>
<p>5.계층화된 시스템(Layered System)
계층화된 구조로 구성되어야 한다. 각 계층 자신이 통신하는 컴포넌트 외 다른 계층에 대한 정보를 얻을 수 없도록 분리되어야 한다.</p>
<h2 id="rest-api-설계-가이드-라인">REST API 설계 가이드 라인</h2>
<ul>
<li>명사형 리소스 사용하지 않는다.
REST API NAMING을 위해서는 동사보다는 명사를 사용하는 것이 좋다.
REST는 명사를 사용하여 리소스를 표현하고, HTTP METHOD를 이용하여 API의 동작을 표현하여 동사 역할을 하기 때문이다.</li>
</ul>
<pre><code class="language-http">- BAD CASE
&lt;https://api.example.com/members/getCart
- GOOD CASE
GET &lt;https://api.example.com/members/cart&gt;</code></pre>
<ul>
<li>복수형 명사를 사용한다.
REST API의 RESOURCE는 자원이라고 볼 수 있다.
RESOURCE는 단일 자원이 아닌 여러개의 자원(컬렉션)을 의미하므로 
복수형을 사용하는 것이 자연스럽다.
그러므로 식별자를 추가로 사용하여 단일 자원을 얻을 수 있다.</li>
</ul>
<pre><code class="language-http">- PLURAL CASE
GET https://api.example.com/orders
- SINGULAR CASE
GET https://api.example.com/orders/:orderId</code></pre>
<blockquote>
<p>팀에 따라 단일 자원을 반환 RESOURCE는 단수형 명사를 사용하기도 한다.</p>
</blockquote>
<ul>
<li>언버바(_) 사용하지 말고 하이픈(-)을 사용하라.
URI를 쉽게 읽고 해석하기 위해서 하이픈(-)을 사용해 가독성을 높일 수 있다.</li>
</ul>
<pre><code class="language-http">- BAD CASE
GET &lt;https://api.example.com/productOptions/:optionId&gt;
- GOOD CASE
GET &lt;https://api.example.com/product-options/:optionId&gt;</code></pre>
<ul>
<li>소문자를 사용하라.
URL에서는 프로토콜과 호스트 주소는 대소문자를 구분하지 않지만 운영체제에 따라 구분하는 경우가 있으므로 소문자로 통일해서 사용하는것이 관례이다.</li>
</ul>
<pre><code class="language-http">- BAD CASE
&lt;https://api.example.com/products/1/Reviews&gt;
- GOOD CASE
&lt;https://api.example.com/products1/reviews&gt;</code></pre>
<ul>
<li>리소스에 대한 식별이 필요한 경우에 PATH PARAMETER(/{ID})방식을 사용한다.</li>
</ul>
<pre><code class="language-http">- BAD CASE
GET &lt;https://api.example.com/orders/orderId?order-id=1&gt;
- GOOD CASE
GET &lt;https://api.example.com/orders/1&gt;</code></pre>
<ul>
<li>컬렉션 필터링이 필요한 경우 쿼리 파라미터(query parameter)를  사용한다.(예시: 페이지네이션, 정렬 등)</li>
</ul>
<pre><code class="language-http">- BAD CASE
GET &lt;https://api.example.com/products/page/2/size/20&gt;
- GOOD CASE
GET &lt;https://api.example.com/products?page=2&amp;size=20&gt;
</code></pre>
<h2 id="rest-api-버전-관리-방법">REST API 버전 관리 방법</h2>
<h3 id="uri-versioning">URI versioning</h3>
<pre><code class="language-http">https://api.example.com/v1/members/1</code></pre>
<h3 id="request-parameter-versioning">Request Parameter versioning</h3>
<pre><code class="language-http">https://api.example.com/members/1?version=1
</code></pre>
<h3 id="mime-type-versioning">MIME type versioning</h3>
<pre><code class="language-http">https://api.example.com/members/1
Accept: application/vnd.example.v1+json
</code></pre>
<h3 id="header-versioning">Header versioning</h3>
<pre><code class="language-http">https://api.example.com/members/1

- http header
API-Version: 1
</code></pre>
<p>가장 많이 사용되는 방식은 URI versioning이다.</p>
<h2 id="요청응답-규약-전략">요청/응답 규약 전략</h2>
<h3 id="content-type--accept-헤더">Content-Type / Accept 헤더</h3>
<p>요청(Request) 시에는 Content-Type으로 본문(body)의 형식을 명시한다.
응답(Response) 시에는 Accept 헤더를 참고하여 클라이언트가 원하는 형식(JSON, XML 등)을 반환한다.
일반적으로 REST API에서는 application/json을 기본으로 사용한다.</p>
<pre><code class="language-http">- 요청 예시
POST /api/v1/members HTTP/1.1
Content-Type: application/json
Accept: application/json</code></pre>
<h3 id="응답-래핑-response-wrapping">응답 래핑 (Response Wrapping)</h3>
<p>가능하다면 리소스 자체를 그대로 반환한다.</p>
<p>불필요하게 data, result 같은 추가 래퍼(wrapper) 객체로 감싸지 않는다.</p>
<pre><code class="language-json">- BAD CASE
{
  &quot;data&quot;: {
    &quot;id&quot;: 1,
    &quot;name&quot;: &quot;Alice&quot;
  }
}

- GOOD CASE
{
  &quot;id&quot;: 1,
  &quot;name&quot;: &quot;Alice&quot;
}</code></pre>
<h3 id="빈-컬렉션-처리">빈 컬렉션 처리</h3>
<p>빈 배열은 null이 아닌 빈 배열([]) 로 반환한다.</p>
<p>이 방식이 클라이언트 단에서 처리하기 가장 일관적이다.</p>
<pre><code class="language-json">- BAD CASE
NULL
- GOOD CASE
[], {}</code></pre>
<h3 id="없는-리소스-처리">없는 리소스 처리</h3>
<p>존재하지 않는 단일 리소스는 404 Not Found 상태코드로 반환한다.
빈 응답 대신 명확한 상태코드를 사용해야 한다.</p>
<pre><code class="language-json">HTTP/1.1 404 Not Found
Content-Type: application/json

{
  &quot;errorCode&quot;: &quot;MEMBER_NOT_FOUND&quot;,
  &quot;message&quot;: &quot;해당 회원을 찾을 수 없습니다.&quot;
}</code></pre>
<h2 id="고민">고민</h2>
<h3 id="rest-중첩-리소스를-어떻게-해결해야-하는가">REST 중첩 리소스를 어떻게 해결해야 하는가?</h3>
<p>소규모 프로젝트에서는 리소스 중첩의 Depth가 얕아 크게 문제가 되지 않는다. 하지만 프로젝트 규모가 커질수록 중첩 구조가 점점 깊어질 수 있고, 그럴 경우 URL의 가독성과 유지보수성이 급격히 떨어진다.
이를 해결하기 위해 보통 중첩 구조(Nested Resource) 를 평탄화(Flat Resource) 방식으로 풀어내는 전략을 사용한다.
예시:</p>
<h4 id="2중첩">2중첩</h4>
<ul>
<li>nested resource<pre><code class="language-http"># 스토어의 상품 목록
GET /stores/10/products
</code></pre>
</li>
</ul>
<h1 id="상품의-리뷰-목록">상품의 리뷰 목록</h1>
<p>GET /products/200/reviews</p>
<h1 id="주문의-아이템-목록">주문의 아이템 목록</h1>
<p>GET /orders/555/items</p>
<pre><code>- flat resource
```http
# 스토어의 상품 목록
GET /products?storeId=10

# 상품의 리뷰 목록
GET /reviews?productId=200

# 주문의 아이템 목록
GET /order-items?orderId=555

</code></pre><h4 id="3중첩">3중첩</h4>
<ul>
<li>nested resource<pre><code class="language-http"># 스토어의 특정 카테고리 상품 목록
GET /stores/10/categories/3/products
</code></pre>
</li>
</ul>
<h1 id="고객의-특정-주문의-아이템-목록">고객의 특정 주문의 아이템 목록</h1>
<p>GET /customers/99/orders/555/items</p>
<h1 id="상품의-특정-옵션variant의-재고-이력">상품의 특정 옵션(variant)의 재고 이력</h1>
<p>GET /products/200/variants/12/inventories</p>
<pre><code>- flat resource
```http
# 스토어의 특정 카테고리 상품 목록
GET /products?storeId=10&amp;categoryId=3

# 고객의 특정 주문의 아이템 목록
GET /order-items?customerId=99&amp;orderId=555

# 상품의 특정 옵션(variant)의 재고 이력
GET /inventories?productId=200&amp;variantId=12
</code></pre><h4 id="4중첩">4중첩</h4>
<ul>
<li>nested resource<pre><code class="language-http"># 스토어 → 카테고리 → 상품 → 리뷰
GET /stores/10/categories/3/products/200/reviews
</code></pre>
</li>
</ul>
<h1 id="고객-→-주문-→-아이템-→-반품">고객 → 주문 → 아이템 → 반품</h1>
<p>GET /customers/99/orders/555/items/7/returns</p>
<h1 id="스토어-→-상품-→-옵션variant-→-가격-변경-이력">스토어 → 상품 → 옵션(variant) → 가격 변경 이력</h1>
<p>GET /stores/10/products/200/variants/12/price-changes</p>
<pre><code>- flat resource
```http
# 스토어 → 카테고리 → 상품 → 리뷰
GET /reviews?storeId=10&amp;categoryId=3&amp;productId=200

# 고객 → 주문 → 아이템 → 반품
GET /returns?customerId=99&amp;orderId=555&amp;itemId=7

# 스토어 → 상품 → 옵션(variant) → 가격 변경 이력
GET /price-changes?storeId=10&amp;productId=200&amp;variantId=12
</code></pre><p>직접 예시를 만들어 보면, 3중첩부터는 평탄화(flat) 하는 편이 가독성과 재사용성 측면에서 유리해 보인다.
2중첩의 경우도 상황에 따라 flat 대안이 좋을 수 있지만, 예를 들어 GET /products/200/reviews처럼 목록을 “어느 문맥에서 보느냐”가 중요한 경우에는 2중첩을 유지하는 것이 더 자연스러울 때가 있다.</p>
<p>결국 Path Param은 리소스 식별, Query Param은 필터/정렬/페이지네이션, Body는 리소스 표현(생성/수정 데이터) 이라는 원칙 아래 상황에 맞게 유연하게 선택하는 것이 바람직하다. 단일 리소스 조회/수정/삭제는 가능하면 flat으로 두고, 목록·생성 컨텍스트는 2중첩까지는 허용, 3중첩 이상은 flat로 풀기를 기본 가이드로 삼는것이 바람직해 보인다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java로 Stack 구현하는 방법]]></title>
            <link>https://velog.io/@noah-wilson0/Java%EB%A1%9C-Stack-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@noah-wilson0/Java%EB%A1%9C-Stack-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sat, 13 Sep 2025 10:02:07 GMT</pubDate>
            <description><![CDATA[<h1 id="stack이란">Stack이란?</h1>
<p>Stack은 사전 정의 그대로 &quot;쌓다&quot;라는 뜻을 가지고 있다. 즉, 접시에 음식을 쌓아 올리듯 데이터를 차곡차곡 쌓아올리는 것을 말한다.
이렇게 아래 그림과 같이 스택은 마지막에 저장한 데이터를 가장 먼저 꺼내게 되는데 이러한 자료 구조를 LIFO구조라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/69b0737c-c36e-49df-ac4c-5338b4bba179/image.png" alt=""></p>
<h1 id="java의-stack이란">Java의 Stack이란?</h1>
<p>java의 stack class는 vactor class를 상속 받고 있으므로 Thread-Safe하다는 특징이 있다.</p>
<p>추가로 java로 stack을 구현할경우 java.util.stack을 사용하여 구현 해도  되지만 Java의 Stack 구현체인 Vector는 동기화된 메서드로 구현되어 있어 멀티 쓰레드 환경에 안전하게 구현되어 있기 때문에 단일 쓰레드 환경에서는 성능상 불리하다.
한 예로 아래 Stack Class의 push 메서드의 구현체를 보면 synchronized가 붙어 있다.</p>
<ul>
<li><p>stack.push()</p>
<pre><code class="language-java">  public E push(E item) {
      addElement(item);

      return item;
  }
</code></pre>
</li>
</ul>
<pre><code>- stack.push()의 구현체 addElement()
```java

public synchronized void addElement(E obj) {
        modCount++;
        add(obj, elementData, elementCount);
    }
</code></pre><p><a href="https://docs.oracle.com/javase/8/docs/api/java/util/Stack.html">Java 공식문서</a>에서도 스택을 Stack Class보다  Deque Class를 사용하라고 권장하고 있다.</p>
<p>정리하자면 나는 취준 코테를 준비하므로 java.util.stack class를 이용할 생각이다. 
코딩테스트 풀이 예시들을 보면 대부분 Stack 클래스를 활용해 구현하는 경우가 많다. 다만, 시간 제한이나 성능이 중요한 문제에서는 Deque를 스택처럼 활용하는 것이 더 효율적인 선택이 될 수 있다.</p>
<h1 id="java로-구현해보기">Java로 구현해보기</h1>
<pre><code class="language-java">package stack;

import java.util.ArrayDeque;
import java.util.Deque;

public class Stack {
    public static void main(String[] args) {
        //stack 생성
        java.util.Stack&lt;String&gt; stack = new java.util.Stack&lt;&gt;();
        stack.push(&quot;A&quot;);
        stack.push(&quot;B&quot;);
        stack.push(&quot;C&quot;);

        // 1. push() : 데이터 삽입
        System.out.println(&quot;push 후 stack: &quot;+stack);

        // 2. peek() : 최상단 원소 확인 (삭제 안 됨)
        System.out.println(&quot;peek(): &quot; + stack.peek());
        System.out.println(&quot;peek 후 stack: &quot; + stack);

        // 3. pop() : 최상단 원소 꺼내기
        System.out.println(&quot;pop(): &quot; + stack.pop());
        System.out.println(&quot;pop 후 stack: &quot; + stack);

        //4. 성능 측정 테스트
        runBenchmark();


    }

    /**
     * 2. Stack vs Deque 성능 비교 메서드
     */
    private static void runBenchmark() {
        System.out.println(&quot;\n=== Stack vs Deque 성능 비교 ===&quot;);
        for (int n = 1; n &lt;= 1_000_000; n *= 10) {
            long stackTime = benchmarkStack(n);
            long dequeTime = benchmarkDeque(n);

            System.out.printf(&quot;[N=%d] Stack: %.3f ms | Deque: %.3f ms%n&quot;,
                    n, stackTime / 1_000_000.0, dequeTime / 1_000_000.0);
        }
    }

    /**
     *  Stack 성능 측정
     */
    private static long benchmarkStack(int n) {
        java.util.Stack&lt;Integer&gt; stack = new java.util.Stack&lt;&gt;();
        long start = System.nanoTime();
        for (int i = 0; i &lt; n; i++) stack.push(i);
        for (int i = 0; i &lt; n; i++) stack.pop();
        return System.nanoTime() - start;
    }

    /**
     *  Deque 성능 측정
     */
    private static long benchmarkDeque(int n) {
        Deque&lt;Integer&gt; deque = new ArrayDeque&lt;&gt;();
        long start = System.nanoTime();
        for (int i = 0; i &lt; n; i++) deque.push(i);
        for (int i = 0; i &lt; n; i++) deque.pop();
        return System.nanoTime() - start;
    }
}
</code></pre>
<p>실행결과:</p>
<pre><code class="language-log">push 후 stack: [A, B, C]
peek(): C
peek 후 stack: [A, B, C]
pop(): C
pop 후 stack: [A, B]

=== Stack vs Deque 성능 비교 ===
[N=1] Stack: 0.008 ms | Deque: 0.006 ms
[N=10] Stack: 0.005 ms | Deque: 0.004 ms
[N=100] Stack: 0.037 ms | Deque: 0.027 ms
[N=1000] Stack: 0.308 ms | Deque: 0.343 ms
[N=10000] Stack: 0.579 ms | Deque: 0.635 ms
[N=100000] Stack: 3.794 ms | Deque: 2.663 ms
[N=1000000] Stack: 21.302 ms | Deque: 19.855 ms

Process finished with exit code 0
</code></pre>
<p>전체적으로 Deque를 활용한 스택 구현이 Stack 클래스보다 성능 면에서 우위를 보였다. 따라서 코딩테스트에서는 큰 차이가 없을 수 있지만, 실제 프로젝트 환경에서는 Deque를 사용하는 것이 더 바람직한 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security+JWT 인증된 사용자 요청 처리 -4]]></title>
            <link>https://velog.io/@noah-wilson0/Spring-SecurityJWT-%EC%9D%B8%EC%A6%9D%EB%90%9C-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9A%94%EC%B2%AD-%EC%B2%98%EB%A6%AC-4</link>
            <guid>https://velog.io/@noah-wilson0/Spring-SecurityJWT-%EC%9D%B8%EC%A6%9D%EB%90%9C-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9A%94%EC%B2%AD-%EC%B2%98%EB%A6%AC-4</guid>
            <pubDate>Thu, 22 May 2025 10:33:48 GMT</pubDate>
            <description><![CDATA[<p>지금까지 Spring Security와 JWT를 활용한 로그인 기능을 구현해보았다.
이제는 로그인된 사용자의 요청을 어떻게 처리할 것인지에 대해 정리해보고자 한다.
우선, 인증된 사용자 요청의 처리 흐름을 단계별로 정리하여 전체적인 구조를 명확히 이해하고자 한다.</p>
<h1 id="인증된-사용자-요청-처리-과정">인증된 사용자 요청 처리 과정</h1>
<ol>
<li>사용자가 로그인 요청을 보내고, 서버는 인증에 성공하면 Access Token과 Refresh Token이 포함된 JWT를 발급한다.</li>
<li>JWT는 Bearer 방식이므로, 클라이언트는 이후 요청 시 Authorization 헤더에 Bearer &lt;Access Token&gt;형식으로 토큰을 포함해 서버에 요청을 보낸다.</li>
<li>서버는 Spring Security의 FilterChain을 통해 요청을 가로채고,
JwtAuthenticationFilter에서 Authorization 헤더를 추출하여 JwtTokenProvider.validateToken()을 통해 토큰의 유효성을 검증한다.
이후 검증이 성공하면 SecurityContextHolder에 인증 정보를 등록한다.</li>
<li>인증이 완료된 요청만 Spring Security의 인증 절차를 통과하여 Controller로 전달되며, 컨트롤러는 해당 요청을 처리하게 된다.</li>
<li>예를 들어, 사용자 정보를 조회하는 요청의 경우 컨트롤러에서는 @AuthenticationPrincipal 또는 SecurityContextHolder를 통해
현재 인증된 사용자 정보를 조회한 후 응답을 반환한다.</li>
</ol>
<p>코딩을 시작하기 전 이런 의문이 생겼다.</p>
<h1 id="securitycontextholder의-인증-정보가-계속-유지되는가">SecurityContextHolder의 인증 정보가 계속 유지되는가?</h1>
<p><strong>filterChain</strong></p>
<pre><code class="language-java">    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic(Customizer.withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -&gt;session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -&gt; auth
                        .requestMatchers(&quot;/members/login&quot;,&quot;/members/sign-in&quot;, &quot;/members/sign-in/test&quot;).permitAll()
                        .requestMatchers(&quot;/members/test&quot;).hasRole(&quot;USER&quot;)
                        .anyRequest().authenticated()
                )
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class).build();
    }</code></pre>
<p>위 SecurityConfig에서 SessionCreationPolicy.STATELESS 설정을 보면 알 수 있듯,
JWT는 상태를 저장하지 않는(Stateless) 인증 방식이다.
즉, 서버는 로그인 상태나 인증 세션을 별도로 유지하지 않는다.</p>
<p>또한 Spring Security의 SecurityContextHolder는 ThreadLocal 기반으로 동작하므로,
하나의 HTTP 요청에 대해서만 인증 정보를 유지하고,
요청이 끝나면 해당 Context는 자동으로 사라진다.</p>
<p>따라서 &quot;한 번 인증된 사용자의 정보가 계속 유지되는가?&quot; 같은 고민은 하지 않아도 된다.
모든 요청은 JWT 토큰을 통해 매번 독립적으로 인증되기 때문이다.</p>
<h1 id="securitycontextholder-사용방식">SecurityContextHolder 사용방식</h1>
<p><strong>SecurityConfig</strong></p>
<pre><code class="language-java">http.authorizeHttpRequests(auth -&gt; auth
        .requestMatchers(&quot;/members/test&quot;).hasRole(&quot;USER&quot;)
        .anyRequest().authenticated()
);
http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
</code></pre>
<p>SecurityConfig에서 인증된 사용자만 접근 가능하도록 엔드포인트를 지정하고 
JWT 인증 필터를 등록한다.</p>
<p><strong>MemberController</strong></p>
<pre><code class="language-java">
    @GetMapping(&quot;/test&quot;)
    public ResponseEntity&lt;MemberInfo&gt; getMemberTest() {
        Member member = memberService.getMemberInfo();
        return ResponseEntity.ok(MemberInfo.builder()
                .name(member.getName())
                .signInId(member.getUsername())
                .email(member.getEmail())
                .gender(member.getGender())
                .birthday(member.getBirthday())
                .score(member.getScore())
                .level(member.getLevel())
                .win(member.getWin())
                .lose(member.getLose())
                .build());
    }</code></pre>
<p>인증이 완료된 요청을 처리하기 위한 Controller를 설계한다.</p>
<p><strong>MemberService</strong></p>
<pre><code class="language-java">    public Member getMemberInfo() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String userName = authentication.getName();
        log.info(&quot;인증된 사용자 이름:{}&quot;, userName);

        return memberRepository.findByUsername(userName)
                .orElseThrow(() -&gt; new UserNotFoundException(userName));

    }</code></pre>
<p>JwtAuthenticationFilter에서 JWT의 유효성 검증이 완료되면, 해당 토큰의 사용자 정보를 담은 Authentication 객체가 SecurityContextHolder에 저장되었기 때문에</p>
<p>MemberService에서 SecurityContextHolder.getContext().getAuthentication()을 통해 인증된 사용자의 username을 조회할 수 있으며,
이를 통해 DB에서 해당 사용자의 상세 정보를 조회할 수 있다.</p>
<h3 id="요청-결과">요청 결과</h3>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/ab56e2b8-257b-49e1-a30f-d98c97ec41f2/image.png" alt=""></p>
<p>인증 및 권한 확인이 모두 완료되었기 때문에,
서버는 요청을 성공적으로 처리하고 클라이언트에 HTTP 200 OK 응답과 함께 결과를 반환한다.</p>
<h1 id="authenticationprincipal-사용방식">@AuthenticationPrincipal 사용방식</h1>
<p>@AuthenticationPrincipal은 Spring Security에서 현재 인증된 사용자의 정보를 컨트롤러 메서드 파라미터로 주입받을 수 있도록 도와주는 어노테이션이다.</p>
<p>처음에는 단순히 @AuthenticationPrincipal과 Member를 사용하면 바로 사용자 정보를 조회할 수 있을 거라 생각했다.</p>
<p><strong>MemberController</strong></p>
<pre><code class="language-java">@GetMapping(&quot;/test&quot;)
    public ResponseEntity&lt;MemberInfo&gt; getMemberTest(@AuthenticationPrincipal Member member) {

                return ResponseEntity.ok(MemberInfo.builder()
                .name(member.getName())
                .signInId(member.getUsername())
                .email(member.getEmail())
                .gender(member.getGender())
                .birthday(member.getBirthday())
                .score(member.getScore())
                .level(member.getLevel())
                .win(member.getWin())
                .lose(member.getLose())
                .build());
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/de6b17f7-19a1-49e0-b9ea-fa539efecdee/image.png" alt=""></p>
<p>하지만 실제 실행 결과는 500 서버 오류였다.</p>
<p>로그를 확인해보니 다음과 같은 NPE(NullPointerException)가 발생했다</p>
<pre><code class="language-java">java.lang.NullPointerException: Cannot invoke &quot;com.agora.debate.member.entity.Member.getName()&quot; because &quot;member&quot; is null</code></pre>
<h3 id="문제-원인-분석">문제 원인 분석</h3>
<p>분명히 JWT 토큰을 검증하는 JwtAuthenticationFilter에서 다음과 같이 User를 등록했었다.
**
JwtTokenProvier.getAuthentication()**</p>
<pre><code class="language-java">        UserDetails principal = new User(claims.getSubject(), &quot;&quot;, authorities);
        return new UsernamePasswordAuthenticationToken(principal, &quot;&quot;, authorities);</code></pre>
<p>문제는 principal에 Spring Security의 기본 User 객체를 사용했다는 점이다.</p>
<p>컨트롤러에서는 @AuthenticationPrincipal Member member로 주입을 받으려 했기 때문에, 타입이 맞지 않아 null이 주입된 것이다.</p>
<p>즉, SecurityContextHolder에 등록된 객체는 User인데,
@AuthenticationPrincipal은 이를 Member로 캐스팅할 수 없으니 null이 된 것이다.</p>
<pre><code class="language-java">        Member member = memberRepository.findByUsername(claims.getSubject())
                .orElseThrow(() -&gt; new UsernameNotFoundException(&quot;사용자를 찾을 수 없습니다.&quot;));

        return new UsernamePasswordAuthenticationToken(member, &quot;&quot;, authorities);</code></pre>
<h3 id="해결방법">해결방법</h3>
<p>JwtTokenProvider.getAuthentication()에서 User 대신 내가 직접 구현한 Member를 principal로 등록하면 된다.</p>
<p>이렇게 수정하면 SecurityContextHolder에 등록된 객체가 Member가 되므로,
컨트롤러에서 @AuthenticationPrincipal Member member를 통해 정상적으로 사용자 정보를 주입받을 수 있다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/d2e20bee-70af-448a-bb47-066dd1ed4bc0/image.png" alt=""></p>
<p>@AuthenticationPrincipal을 사용하려면 SecurityContextHolder에 등록되는 principal 객체가
실제로 주입받으려는 타입(Member 또는 CustomUserDetails)과 일치해야 한다.</p>
<h1 id="결론">결론</h1>
<p>처음에는 SecurityContextHolder.getContext().getAuthentication()를 통해 인증 정보를 직접 꺼내서 사용하는 방식도 고려했지만,
코드의 가독성과 의도를 명확하게 전달하기 위해 @AuthenticationPrincipal을 사용하는 방식으로 정리하였다.</p>
<p>이 어노테이션을 사용하면 컨트롤러에서 바로 인증된 사용자 객체를 주입받을 수 있어 코드가 훨씬 간결해지고, 비즈니스 로직에 집중할 수 있는 장점이 있다.</p>
<p>따라서 이후 인증된 사용자 정보가 필요한 컨트롤러에서는 @AuthenticationPrincipal을 통해 일관성 있게 처리할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security+JWT 로그인 구현해보기 -3]]></title>
            <link>https://velog.io/@noah-wilson0/Spring-SecurityJWT-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0-3</link>
            <guid>https://velog.io/@noah-wilson0/Spring-SecurityJWT-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0-3</guid>
            <pubDate>Wed, 21 May 2025 07:57:50 GMT</pubDate>
            <description><![CDATA[<h2 id="1-라이브러리-설정">1. 라이브러리 설정</h2>
<p>build.gradle</p>
<pre><code class="language-java">    //스프링 시큐리티
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
    implementation &#39;io.jsonwebtoken:jjwt-api:0.11.5&#39;
    runtimeOnly &#39;io.jsonwebtoken:jjwt-impl:0.11.5&#39;
    runtimeOnly &#39;io.jsonwebtoken:jjwt-jackson:0.11.5&#39;</code></pre>
<h2 id="2-jwt-token-dto-생성">2. JWT Token DTO 생성</h2>
<p>클라이언트에게 토큰을 발급해주기 위해 JwtToken DTO을 생성해준다.</p>
<pre><code class="language-java">@Builder
@Data
@AllArgsConstructor
public class JwtToken {
    private String grantType;
    private String accessToken;
    private String refreshToken;
}</code></pre>
<p>JwtToken의 필드중 grantType은 JWT에 대한 인증 타입이다. 인증 타입에 대해서 잘 모르므로 널리 사용되는 &quot;Bearer&quot;방식을 사용할 것이다.
ex) Authorization: Bearer <access_token></p>
<blockquote>
<p>Bearer 인증 방식이란 ? 
ACCESS TOKEN을 HTTP REQEUST HEADER의 Authorization을 포함하여 전송하는 방식이다.  </p>
</blockquote>
<h2 id="3-암호키-설정">3. 암호키 설정</h2>
<p>JWT를 만들 때, 토큰의 서명(Signature)을 생성하는 데 사용할 암호화 키를 설정해야 한다.</p>
<pre><code>openssl rand -hex 32</code></pre><p>생성된 secret key를 application.properties에서 설정한다.(필자의 경우,  application-scret.properties에 설정함)
application-scret.properties</p>
<pre><code class="language-java">jwt.secret=your_secret_key</code></pre>
<h2 id="4-jwttokenprovider-구현">4. JwtTokenProvider 구현</h2>
<p>Spring Security와 JWT 토큰을 사용하여 인증과 권한 부여를 처리하는 클래스이다.JWT 토큰의 생성, 복호화, 검증 기능을 구현한 클래스이다.
이후 로그인 기능에서 JWT를 생성하고 JwtAuthenticationFilter에서 유효성 검사를 하는데 사용된다.</p>
<pre><code class="language-java">@Component
public class JwtTokenProvider {
    private final Key key;

    public JwtTokenProvider(@Value(&quot;${jwt.secret}&quot;) String secretKey) {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    public JwtToken generateToken(Authentication authentication) {
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(&quot;,&quot;));

        long now = (new Date()).getTime();

        Date accessTokenExpiresIn = new Date(now + 86400000);
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim(&quot;auth&quot;, authorities)
                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        String refreshToken = Jwts.builder()
                .setExpiration(new Date(now + 86400000))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        return JwtToken.builder()
                .grantType(&quot;Bearer&quot;)
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    public Authentication getAuthentication(String accessToken) {
        // Jwt 토큰 복호화
        Claims claims = parseClaims(accessToken);

        if (claims.get(&quot;auth&quot;) == null) {
            throw new RuntimeException(&quot;권한 정보가 없는 토큰입니다.&quot;);
        }

        Collection&lt;? extends GrantedAuthority&gt; authorities = Arrays.stream(claims.get(&quot;auth&quot;).toString().split(&quot;,&quot;))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        UserDetails principal = new User(claims.getSubject(), &quot;&quot;, authorities);
        return new UsernamePasswordAuthenticationToken(principal, &quot;&quot;, authorities);
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info(&quot;Invalid JWT Token&quot;, e);
        } catch (ExpiredJwtException e) {
            log.info(&quot;Expired JWT Token&quot;, e);
        } catch (UnsupportedJwtException e) {
            log.info(&quot;Unsupported JWT Token&quot;, e);
        } catch (IllegalArgumentException e) {
            log.info(&quot;JWT claims string is empty.&quot;, e);
        }
        return false;
    }


    // accessToken
    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(accessToken)
                    .getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }

}</code></pre>
<h3 id="generatetoken">generateToken()</h3>
<p>Spring Security의 Authentication 객체에서 권한정보(Authorities)를 추출한 후, 이를 쉼표(,)로 구분된 문자열로 반환하여 JWT의 클레임(auth)에 저장할 수 있도록 가공한다.</p>
<p>AccessToken는 다음과 같이 생성된다.
<strong>Header</strong>
기본적인 헤더는 자동으로 설정되므로 따로 설정하지 않았다. 
<strong>Payload</strong>
만료시간(exp): setExpiration(accessTokenExpiresIn)
제목(sub): setSubject(authentication.getName())
권한정보(auth): claim(&quot;auth&quot;, authorities)
<strong>Signature</strong>
signWith(key, SignatureAlgorithm.HS256)
를 설정하고 signature(signWith)을 설정해준다.</p>
<p>refreshToken은 Access Token을 재발급받기 위한 용도로 사용되므로 만료시간,과 서명정보만 생성한다.</p>
<h2 id="5-getauthentication">5. getAuthentication()</h2>
<p>Access token을 복호화하여 사용자의 인증 정보(Authentication)를 생성한다.
parseClaims()를 사용하여 권한 정보를 추출하여 권한 정보가 없을경우 RuntimeException을 일으킨다.</p>
<p>Collection&lt;? extends GrantedAuthority&gt;로 리턴받는 이유?
권한 정보를 다양한 타입의 객체로 처리할 수 기 떄문이다.</p>
<blockquote>
<p>GrantedAuthority란? 
Authorities이라는 목록의 타입이다. Collection<GrantedAuthority>와 같은 형태를 의미한다.
필자는 GrantedAuthority의 대표적 구현체인 SimpleGrantedAuthority를 사용했다. SimpleGrantedAuthority는 문자열로 역할(예: &quot;USER&quot;)을 하는 객체이다. 최종적으로 Collection<GrantedAuthority>=[SimpleGrantedAuthority(&quot;USER&quot;),SimpleGrantedAuthority(&quot;ADMIN&quot;)]과 같은 형태를 가진다.</p>
</blockquote>
<blockquote>
<p>Authentication란?
 SicurityContext에 의해  관리하고 Authentication는 principal, Credentials, Authorities로 구성되어 있다. <a href="https://velog.io/@noah-wilson0/Spring-Security-%EC%82%AC%EC%A0%84-%EC%A7%80%EC%8B%9D%EC%9A%A9%EC%96%B4-%EB%B0%8F-%ED%9D%90%EB%A6%84">참고</a></p>
</blockquote>
<h3 id="validatetoken">validateToken()</h3>
<p>토큰의 유효성을 검증한다.
토큰의 서명키를 설정하여 예외 처리를 통해 토큰의 유효성 여부를 판단한다.
<strong>SecurityException | MalformedJwtException</strong> : 서명(Signature)이 잘못되었거나, JWT 형식이 잘못된 경우
<strong>ExpiredJwtException</strong> : 토큰의 exp(만료시간)가 지나서 더 이상 사용할 수 없는 경우
<strong>UnsupportedJwtException</strong> : 지원되지 않는 JWT 구조나 알고리즘인 경우
<strong>IllegalArgumentException</strong> : 토큰이 null이거나 빈 문자열인 경우</p>
<h3 id="parseclaims">parseClaims()</h3>
<p>AccessToken을 복호화하여 서명(Signature)을 검증하고 payload의 claim들을 반환한다.
만료된 토큰인 경우에도 Clam들을 반환한다.</p>
<h2 id="6-jwtauthenticationfilter-구현">6. JwtAuthenticationFilter 구현</h2>
<p>클라이언트 요청 시 JWT 인증을 하기 위해 설치하는 커스텀 필터로
이후 구현할 SecurityConfig에서 addFilterBefore()를 통해  UsernamePasswordAuthenticationFilter 이전에 JwtAuthenticationFilter가 실행되게 할 것이다.</p>
<p>클라이언트의 요청에서 JWT 토큰의 유효성 검사를 하고 해당 토큰의 인증 정보(Authentication)를 Security Context에 저장하여 인증된 요청을 처리할 수 있도록 설정한다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;

        String path = httpRequest.getRequestURI();

        // 필터를 적용하지 않을 경로 설정
        if (path.startsWith(&quot;/members/sign-in&quot;) || path.startsWith(&quot;/members/sign-in/test&quot;)
                || path.startsWith(&quot;/members/signup&quot;)) {
            chain.doFilter(request, response);
            return;
        }

        String token = resolveToken(httpRequest);


        if (token != null &amp;&amp; jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(&quot;Authorization&quot;);
        if (StringUtils.hasText(bearerToken) &amp;&amp; bearerToken.startsWith(&quot;Bearer&quot;)) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
</code></pre>
<h3 id="dofilter">doFilter()</h3>
<p>회원가입, 로그인, 필터 작동 테스트에서는 필터가 작동하지 않게 설정.
jwtTokenProvider에서 작성한 validateToken()을 사용하여 토큰의 유효성을 검사하고 jwtTokenProvider.getAuthentication()을 이용하여 Authentication객체를 생성한 후 SecurityContext에 Authentication를 저장한다.</p>
<h3 id="resolvetoken">resolveToken()</h3>
<p>HttpServletRequest의 Header에서 토큰을 추출하여 반환한다.</p>
<blockquote>
<p>Bearer 방식에서는 JWT가 Authorization 헤더에 &quot;Bearer &lt;토큰값&gt;&quot; 형식으로 포함된다.</p>
</blockquote>
<h2 id="7-securityconfig-구현">7. SecurityConfig 구현</h2>
<p>Spring Security의 설정을 담당한다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic(Customizer.withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -&gt;session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -&gt; auth
                        .requestMatchers(&quot;/members/login&quot;,&quot;/members/sign-in&quot;, &quot;/members/sign-in/test&quot;).permitAll()
                        .requestMatchers(&quot;/members/test&quot;).hasRole(&quot;USER&quot;)
                        .anyRequest().authenticated()
                )
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class).build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // BCrypt Encoder 사용
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

}</code></pre>
<h3 id="filterchain">filterChain()</h3>
<p>REST API이므로 basic auth 및 csrf보안을 사용하지 않는다.
JWT를 사용하므로 세션을 사용하지 않는다.
권한별로 요청을 허가하도록 요청에 대한 인가 규칙 설정한다.
JWT 인증을 위하여 직접 구현한 필터를UsernamePasswordAuthenticationFilter 전에 실행한다.</p>
<blockquote>
<p>requestMatchers를 통해 회원가입, 로그인, 필터 작동 테스트의 요청을 권한이 없어도 요청을 허가했지만 JwtAuthenticationFilter에서 다시 필터를 적용하지 않을 경로 설정한 이유?</p>
</blockquote>
<h3 id="passwordencoder">passwordEncoder()</h3>
<p>DelegatingPasswordEncoder를 사용하여 기본 인코딩 알고리즘을 &quot;bcrypt&quot;로 설정하며, 동시에 여러 인코딩 방식을 지원할 수 있는 구조를 설정한다.</p>
<h2 id="8-인증을-위한-entity-repository-설정">8. 인증을 위한 Entity, Repository 설정</h2>
<p>자신의 프로젝트에 맞는 Entity를 설정하고 UserDetails interface를 구현한다.
혼동을 피하기 위해 username은 사용자 ID로 사용되며 인증은 username과 password를 이용하여 진행한다.</p>
<pre><code class="language-java">package com.agora.debate.member.entity;

import com.agora.debate.global.enums.Gender;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * TODO: @EqualsAndHashCode(of = &quot;id&quot;)공부
 */
@Entity
@Table(name = &quot;member&quot;)
@Builder
@EqualsAndHashCode(of = &quot;id&quot;)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@ToString
public class Member implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;member_id&quot;)
    private Long id;

    @Column(nullable = false,unique = true)
    private String name;

    @Column(name = &quot;username&quot;, nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false, unique = true)
    private String email;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Gender gender;

    @Column(name = &quot;birth_date&quot;, nullable = false)
    private LocalDate birthday;

    @Builder.Default
    @Column(nullable = false)
    private int score=100;

    @Builder.Default
    @Column(nullable = false)
    private int level=1;

    @Builder.Default
    @Column(nullable = false)
    private int win=0;

    @Builder.Default
    @Column(nullable = false)
    private int lose=0;

    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = &quot;member_roles&quot;, joinColumns = @JoinColumn(name = &quot;member_id&quot;))
    @Column(name = &quot;role&quot;)
    @Builder.Default
    private List&lt;String&gt; roles = new ArrayList&lt;&gt;();
    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        return this.roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    //Default=true
    @Override
    public boolean isAccountNonExpired() {
        return UserDetails.super.isAccountNonExpired();
    }
    //Default=true
    @Override
    public boolean isAccountNonLocked() {
        return UserDetails.super.isAccountNonLocked();
    }
    //Default=true
    @Override
    public boolean isCredentialsNonExpired() {
        return UserDetails.super.isCredentialsNonExpired();
    }
    //Default=true
    @Override
    public boolean isEnabled() {
        return UserDetails.super.isEnabled();
    }
}
</code></pre>
<h3 id="getauthorites">getAuthorites()</h3>
<p>멤버가 가지고 있는 권한 목록(rloes)을 SimpleGrantedAuthority로 변환하여 반환한다.</p>
<h2 id="memberrepository">MemberRepository</h2>
<pre><code class="language-java">@Repository
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    ....

    Optional&lt;Member&gt; findByUsername(String username);

    ....

}</code></pre>
<h2 id="9-로그인을-위한-signin-service-구현">9. 로그인을 위한 signIn Service 구현</h2>
<pre><code class="language-java">    @Transactional
    public JwtToken signIn(SignInDto signInDto) {
        memberRepository.findByUsername(signInDto.getUsername())
                .orElseThrow(() -&gt; new UserNameNotMatchException());

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(signInDto.getUsername(), signInDto.getPassword());

        Authentication authentication = null;

        try {
            authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        } catch (AuthenticationException e) {
            throw new BadCredentialsException(&quot;authentication실패&quot;);
        }
        JwtToken jwtToken = jwtTokenProvider.generateToken(authentication);
        return jwtToken;
    }</code></pre>
<p>사용자 존재 여부 확인
사용자의 입력값(username, password)을 기반으로 Authentication객체 생성한다.
authenticate() 메서드를 통해 요청된 Member에 대한 검증 진행한다.
이떄 authenticate()가 내부적으로 loadUserByUsername()을 (아래 CustomUserDetailsService()에서 구현 예정)
검증에 성공시 인증된 Authentication객체를 기반으로 jwtTokenProvider.generateToken()을 이용하여 JWT토큰을 생성한다.</p>
<h2 id="10-customuserdetailsservice-구현">10. CustomUserDetailsService 구현</h2>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        return memberRepository.findByUsername(username)
                .map(this::createUserDetails)
                .orElseThrow(() -&gt; new UsernameNotFoundException(&quot;해당하는 회원을 찾을 수 없습니다.&quot;));
    }

    private UserDetails createUserDetails(Member member) {
        return User.builder()
                .username(member.getUsername())
                .password(member.getPassword())
                .roles(member.getRoles().toArray(new String[0]))
                .build();
    }

}</code></pre>
<p>Spring Security에서 제공하는 UserDetailsService 인터페이스를 구현한 클래스이다.
로그인 시 입력된 username을 기반으로 사용자 정보를 조회하고,
Spring Security가 인증에 사용할 수 있도록 UserDetails 객체로 변환해 반환한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring security JWT 개념 정리-2]]></title>
            <link>https://velog.io/@noah-wilson0/Spring-security-JWT-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-2</link>
            <guid>https://velog.io/@noah-wilson0/Spring-security-JWT-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-2</guid>
            <pubDate>Tue, 20 May 2025 09:16:55 GMT</pubDate>
            <description><![CDATA[<h1 id="jwt란">JWT란?</h1>
<p>JWT(Json Web Token)란 인증에 필요한 정보들을 암호호시킨 JSON 토큰을 의미한다. 보통 JWT와 같은 토큰기반 인증 방식은 토큰을 HTTP 헤더에 실어 서버가 클라이언트를 식별하는 방식이다.</p>
<p>JWT 데이터는 BASE-64URL를 통해 인코딩하여 직렬화한것이다. 
토큰 내부의 위변조 방지를 위하여 개인키 암호화 방식을 사용한 전자 서명도 들어 있다. </p>
<blockquote>
<p>직렬화란? 데이터를 저장하거나 전송할 수 있도록 문자열과 같은 형식으로 변환하는 과정을 말한다.</p>
</blockquote>
<p>따라서 서버는 로그인 등의 인증 과정에서 사용자에게 JWT를 발급해준다.
사용자는 이후 요청 시 이 JWT를 함께 전송하고, 서버는 토큰의 유효성을 검증한 뒤 요청을 처리하고 응답을 반환한다.</p>
<blockquote>
<p>사용자가 로그인에 성공하면 서버는 인증 수단으로 JWT를 발급한다. 이 토큰은 자격 증명 역할을 하기 때문에 보안에 특히 주의해야 하며, 브라우저의 로컬스토리지나 세션스토리지에 민감한 정보를 장기간 저장하는 것은 권장되지 않는다.</p>
</blockquote>
<h1 id="jwt-구조">JWT 구조</h1>
<p>JWT는 &quot;.&quot;을 구분자로 직렬화된 세가지 문자열의 조합이다.
&quot;.&quot;을 기준으로 Header, Payload, Signature을 의미한다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/8aacc0a5-0bb9-4513-b255-7ea1edd0d1c6/image.png" alt=""></p>
<h3 id="header에는-jwt에서-사용할-타입과-알고리즘-종류가-담겨져-있다">Header에는 JWT에서 사용할 타입과 알고리즘 종류가 담겨져 있다.</h3>
<p>Header에는 JWT에서 사용할 타입과 알고리즘 종류가 담겨져 있다. 
alg:서명 암호화 알고리즘 종류(HAMC,RSA,ECDSA 등)
typ: 토큰 유형</p>
<h3 id="payload">Payload</h3>
<p>Payload는 사용자 권한 정보와 데이터(Claim)가 들어있다.</p>
<blockquote>
<p>Clam이란? key-value 형식으로 이루어진 한쌍의 정보</p>
</blockquote>
<p>클레임은 Registered claims, Public claims, Private claims으로 세가지로 나뉜다.
Registered claims(미리 정의된 클레임): sub(제목), name(발행자),iat(발행시간), exp(만료시간)등이 있다.</p>
<p>Public claims: 사용자가 정의할 수 있는 클레임 
Private claims: 동의한 당사자들 간에 정보를 공유하기 위해 작성된 클레임으로
등록되지도 않고, 공개되지도 않는다.</p>
<h3 id="signature">Signature</h3>
<p>Signature 는 Header, Payload를 Base64-URL로 인코딩한 후 헤더에서 지정된 알고리즘을 가져와서 서명한다.</p>
<p>Signature 구조는 인코딩된 Header와 Payload외 개인키(secret)을 합쳐서 암호화한 값이다.</p>
<pre><code class="language-java">HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; +
  base64UrlEncode(payload),
  secret)</code></pre>
<p>서명은 메시지가 전송 중에 변경되지 않았는지 확인하는 데 사용되며, 개인 키로 서명된 토큰의 경우 JWT를 보낸 사람이 본인인지 확인할 수도 있습니다.</p>
<blockquote>
<p>Header와 Payload는 단순히 인코딩된 값이기 때문에 제 3자가 복호화 및 조작할 수 있지만, Signature는 서버 측에서 관리하는 비밀키가 유출되지 않는 이상 복호화할 수 없다. 따라서 Signature는 토큰의 위변조 여부를 확인하는데 사용된다.</p>
</blockquote>
<h1 id="jwt-인증-과정">JWT 인증 과정</h1>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/cafeb9f8-2367-4bd9-9919-8ea97a3653c9/image.png" alt=""></p>
<ol>
<li>사용자가 ID, PW를 입력하여 서버에 로그인 인증을 요청한다.</li>
<li>서버는 클라이언트에게 요청받으면 Header, PayLoa, Sinature를 정의한다. 
Header, PayLoa, Sinature를 암호화하며 JWT를 생성하고 클라이언트에게 발급한다.</li>
<li>서버로부터 발급받은 JWT를 저장 후 header에 Authorization을 담아서 요청을 보낸다.</li>
<li>서버는 request header에서 Authorization에 담긴 JWT가 자신의 서버에서 발행한 토큰인지 검증 후 일치한다면 인증을 통과시켜주고 요청을 처리한다.</li>
</ol>
<blockquote>
<p>만약 ACCESS TOKEN의 시간이 만료되었다면 서버는 401 Unauthorized 로 응답한다.
클라이언트가 REFRESH TOKEN을 통해 서버에게 새로운 ACCESS TOKEN을  발급받고 요청을 보내야 한다.</p>
</blockquote>
<h1 id="참고자료">참고자료</h1>
<p>JWT
<a href="https://jwt.io/introduction">https://jwt.io/introduction</a>
<a href="https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC#jwt_%EA%B5%AC%EC%A1%B0">https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC#jwt_%EA%B5%AC%EC%A1%B0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security 사전 지식(용어 및 흐름) -1]]></title>
            <link>https://velog.io/@noah-wilson0/Spring-Security-%EC%82%AC%EC%A0%84-%EC%A7%80%EC%8B%9D%EC%9A%A9%EC%96%B4-%EB%B0%8F-%ED%9D%90%EB%A6%84</link>
            <guid>https://velog.io/@noah-wilson0/Spring-Security-%EC%82%AC%EC%A0%84-%EC%A7%80%EC%8B%9D%EC%9A%A9%EC%96%B4-%EB%B0%8F-%ED%9D%90%EB%A6%84</guid>
            <pubDate>Sat, 17 May 2025 04:49:43 GMT</pubDate>
            <description><![CDATA[<h1 id="개념">개념</h1>
<p><a href="https://docs.spring.io/spring-security/reference/index.html">스프링 시큐리티(Spring Security)</a>는 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크이다.
스프링 시큐리티에서는 주로 서블릿 필터(filter)와 이들로 구성된 필터체인으로의 구성된 위임 <a href="https://docs.spring.io/spring-security/reference/servlet/architecture.html">모델</a>을 사용한다.</p>
<h1 id="용어">용어</h1>
<ul>
<li>접근 주체(Principal): 보호된 리소스에 접근하려는 사용자 또는 시스템이다.
일반적으로 로그인한 사용자 정보를 의미한다.</li>
<li>인증(Authentication): 인증은 &#39;증명하다&#39;라는 의미로 예를 들어, 유저A가 아이디와 비밀번호를 이용하여 로그인하는 과정에서 유저 A가 맞는지 증명하는 것을 말한다.</li>
<li>인가(Authorization): &#39;허가&#39;와 같은 의미로 사용된다. 인증된 사용자가 요청한 리소스를 사용할 수 있는 권한이 있는지 검사하는것을 의미한다.</li>
<li>권한(Roles): 사용자가 시스템에서 수행할 수 있는 기능이나 리소스 접근 권한이다.</li>
</ul>
<h1 id="기본-동작-구조">기본 동작 구조</h1>
<p>스프링 시큐리티의 동작 구조는 <a href="https://docs.spring.io/spring-security/reference/servlet/architecture.html">공식문서</a>에 따르면 다음과 같다.</p>
<p>스프링 시큐리티는 서블릿 필터기반으로 동작한다.
서블릿 필터는 서블릿 실행에 대한 전처리 및 후처리를 실행하는 역할이다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/399a776e-0919-485b-a5aa-1b843a0f32e4/image.png" alt=""></p>
<p>클라이언트가 요청을 보내면, WAS는 이 요청을 웹 컨테이너를 통해 처리한다.
웹 컨테이너는 요청 URI를 기반으로 어떤 서블릿이 처리할지 결정하고,
그 서블릿과 관련된 FilterChain(Filter 목록 + 서블릿)을 생성한다.
이 FilterChain은 먼저 여러 Filter들을 실행하며 요청을 가공하거나 보안을 적용한 뒤,최종적으로 서블릿(예: DispatcherServlet)을 실행해 초기화된 서블릿으로 처리를 넘긴다.</p>
<h2 id="delegatingfilterproxy">DelegatingFilterProxy</h2>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/ed86986a-f2a7-4066-9be1-2d4d4e235748/image.png" alt="">
하지만 서블릿 컨테이너는 Spring에서 정의한 Bean을 인식하지 못한다.
이를 해결하기 위해 Spring은 DelegatingFilterProxy라는 Filter구현체를 제공한다.</p>
<pre><code class="language-java">public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    Filter delegate = getFilterBean(someBeanName); //1
    delegate.doFilter(request, response);  //2
}</code></pre>
<ol>
<li>Spring Bean으로 등록된 Filter를 지연해서 가져온다.</li>
<li>위임된 Filer를 수행한다.</li>
</ol>
<p>이렇게 DelegatingFilterProxy는 서블릿 Filter의의 기능을 Bean Filter로 위임하여 실행할 수 있도록 해준다. 즉, DelegatingFilterProxy는 Bean Filter를 Filter로 위임하는 중간 다리 역할을 해준다. </p>
<h2 id="filterchainproxy">FilterChainProxy</h2>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/3c89d8c1-71f2-4175-b140-deb849056d9f/image.png" alt=""></p>
<p>FilterChainProxy는 스프링 시큐리티(Spring Security)가 제공하는 특수한 Filter 구현체로 SecurityFilterChain를 통해 여러개의 SecurityFilterChain을 위임할 수 있게 해준다. 
FilterChainProxy도 Spring에서 제공하는 Bean이므로 서블릿 컨테이너에 직접 등록되지 않고DelegatingFilterProxy 로 매핑된다.</p>
<h2 id="securityfilterchain">SecurityFilterChain</h2>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/c8fd33ba-cd83-45d3-a8f7-9707bc76bcfd/image.png" alt=""></p>
<p>SecurityFilterChain은 FilterChainProxy가 현재 요청에 대해 어떤 Security Filter를 호출해야 할지 결정하는데 사용된다.</p>
<p>Security Filtersms Bean이지만 DelegatingFilterProxy대신에 FilterChainProxy에 등록된다.
그러므로  FilterChainProxy는 스프링 시큐리티(Spring Sicurity)의 시작점 역할을 한다.
FilterChainProxy는 SecurityFilterChain을 통해 여러 Filter 인스턴스로 위임하여 실행할수 있다. 
<img src="https://velog.velcdn.com/images/noah-wilson0/post/545f1d29-310b-46ca-bba9-0e17862ba771/image.png" alt=""></p>
<p>또한 여러 개의 SecurityFilterChain을 설정해두고,
FilterChainProxy를 통해 요청 URL에 따라 각각의 SecurityFilterChain을 매핑하여 적용할 수 있다.
특정 요청에 대해 스프링 시큐리티가 무시하려면, SecurityFilterChain에 보안 Filter를 0개 설정하면 된다.</p>
<p>그럼 인증(Authentication)된 객체가 어떻게 저장될까?</p>
<h1 id="securitycontextholder">SecurityContextHolder</h1>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/a695164e-2a4b-4fd3-9b9a-bb4ec0605ee7/image.png" alt=""></p>
<p>Spring Security에서는 인증(Authentication) 정보를 SecurityContextHolder를 통해 전역적으로 관리한다.<br>실제로는 인증에 성공한 사용자의 Authentication 객체가 SecurityContextHolder가 보유한 SecurityContext 내부에 저장된다.</p>
<p>이 SecurityContext는 일반적으로 ThreadLocal기반으로 동작하여, 
각 요청 스레드마다 독립된 인증 정보를 안전하게 분리하여 관리할 수 있도록 한다.</p>
<h2 id="authentication">Authentication</h2>
<ul>
<li>Principal 
  유저에 대한 정보를 가지고 있다.</li>
<li>Credentials
  인증에 사용되는 자격 증명 정보 (예: 비밀번호, 토큰 등).<br>인증이 완료되면 보안상의 이유로 <code>null</code>로 설정되는 경우가 많다.<ul>
<li>Authorities
유저의 권한 정보를 가지고 있다.
사용자가 가진 권한 목록을 담고 있으며, 내부적으로는 <code>GrantedAuthority</code>의 리스트로 구성된다.</li>
</ul>
</li>
</ul>
<h1 id="spring-security-structure">Spring Security Structure</h1>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/78521231-cc04-4b94-a30c-28d61b12f844/image.png" alt=""></p>
<ol>
<li><p>Http Request
사용자가 /login 요청을 보냄 (아이디/비밀번호 포함)</p>
</li>
<li><p>AuthenticationFilter
UsernamePasswordAuthenticationFilter가 요청을 가로채고,</p>
</li>
</ol>
<p>사용자가 입력한 아이디/비밀번호를 가지고 UsernamePasswordAuthenticationToken 객체 생성</p>
<ol start="3">
<li><p>AuthenticationManager로 토큰 전달
AuthenticationFilter는 AuthenticationManager에게 위에서 만든 토큰을 넘김</p>
</li>
<li><p>AuthenticationManager는 실제 인증을 AuthenticationProvider에 위임
일반적으로 사용되는 구현체는 ProviderManager임</p>
</li>
<li><p>AuthenticationProvider → UserDetailsService 사용
인증을 처리하기 위해 UserDetailsService의 loadUserByUsername() 호출</p>
</li>
<li><p>UserDetailsService가 DB에서 User 정보 조회
아이디로 사용자를 찾고, 비밀번호, 권한 정보 등을 포함한 UserDetails 객체를 리턴</p>
</li>
<li><p>UserDetails를 기반으로 Authentication 객체 생성
AuthenticationProvider는 아이디/비밀번호가 일치하면,
인증 완료 상태의 Authentication 객체를 생성하고 반환</p>
</li>
<li><p>AuthenticationManager → 인증 성공 결과 반환
최종적으로 인증된 Authentication 객체를 AuthenticationManager가 AuthenticationFilter에 반환</p>
</li>
<li><p>AuthenticationFilter가 인증 성공 시 SecurityContext에 저장
SecurityContextHolder.getContext().setAuthentication(...) 호출</p>
</li>
<li><p>SecurityContextHolder에 인증 정보 저장 완료
이후부터는 이 정보(Authentication)를 통해
사용자 정보, 권한 등을 어디서든 사용할 수 있음 (예: @AuthenticationPrincipal 등)</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[ spring 유효성 검사 방법]]></title>
            <link>https://velog.io/@noah-wilson0/spring-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@noah-wilson0/spring-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Thu, 15 May 2025 10:28:58 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드에서 회원가입 시 사용자는 ID, 비밀번호, 이메일, 생년월일 등 다양한 항목을 입력합니다. 대부분의 프로젝트에서는 프론트엔드에서 1차 유효성 검사를 하고, 백엔드에서 2차 검증을 수행한 뒤 회원가입 로직을 진행한다.</p>
<p>처음에는 아래와 같이 직접 유효성 검사 클래스를 만들어 Controller에서 Service 호출 전에 명시적으로 검증하려고 했다.</p>
<h1 id="초기-설계">초기 설계</h1>
<pre><code class="language-java">public class SignUpValidator {

    public String checkId(String target) {

    }
    public String checkLoginId(String target) {

    }
    public String checkPassword(String target) {

    }
    public String checkEmail(String target) {

    }
    public String checkGender(String target) {

    }
    public LocalDate checkBirthday(LocalDate target) {

    }
}
</code></pre>
<p>초기 설계 방안으로 utils 패키지에 SignUpValidator 클래스를 만들어 controller에서  service로 회원가입 사이클을 시작하기 전에 SignUpValidator를 수행하려고 했다. 하지만 vaild 로직은 어떤 프로젝트에서도 사용되므로 관련 애노테이션이 있다고 생각하고 찾아보았다.</p>
<p>Spring에서는 이미 이를 위해 표준화된 @Valid, @Validate, @GroupSequence등 여러 애노테이션을 제공하고 있었다.</p>
<h2 id="valid란">@Valid란?</h2>
<p>@Valid는 JSR-303 표준 스펙으로써 빈 검증기(Bean Validator)를 이용해 객체의 제약 조건을 검증하도록 지시하는 어노테이션이다.
JSR 표준의 빈 검증 기술의 특징은 DTO나 VO 객체의 필드에 달린 어노테이션으로 편리하게 검증을 한다는 것이다.</p>
<h4 id="valid-사용법">@Valid 사용법</h4>
<pre><code class="language-java"> @PostMapping
    public String checkId(@RequestBody @Valid SignUpDto signUpDto,
    Errors errors) {}</code></pre>
<pre><code class="language-java">public class SignUpDto {

    @NotBlank
    private String name;

    @NotBlank
    private String loginId;

    @Pattern(regexp = &quot;(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\\\W)(?=\\\\S+$).{8,16}\n&quot;)
    private String password;
    }
</code></pre>
<p>이렇게 유효성 검사가 필요한 Request 객체에 @Valid 어노테이션을 사용해 유효성 검사를 적용할 수 있다.</p>
<h4 id="유효성-검사-애노테이션-종류">유효성 검사 애노테이션 종류</h4>
<table>
<thead>
<tr>
<th>애노테이션</th>
<th>설명</th>
<th>적용 대상</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Null</code></td>
<td>값이 <code>null</code>이어야 함</td>
<td>모든 타입</td>
</tr>
<tr>
<td><code>@NotNull</code></td>
<td><code>null</code>이 아니어야 함</td>
<td>모든 타입</td>
</tr>
<tr>
<td><code>@NotEmpty</code></td>
<td><code>null</code>이 아니고, 길이가 0보다 커야 함 (문자열, 컬렉션 등)</td>
<td>문자열, 컬렉션, 배열</td>
</tr>
<tr>
<td><code>@NotBlank</code></td>
<td><code>null</code>, 빈 문자열, 공백이 아니어야 함</td>
<td>문자열</td>
</tr>
<tr>
<td><code>@AssertTrue</code></td>
<td>값이 <code>true</code>여야 함</td>
<td>boolean, Boolean</td>
</tr>
<tr>
<td><code>@AssertFalse</code></td>
<td>값이 <code>false</code>여야 함</td>
<td>boolean, Boolean</td>
</tr>
<tr>
<td><code>@Min(value)</code></td>
<td>지정된 최소값 이상이어야 함</td>
<td>숫자</td>
</tr>
<tr>
<td><code>@Max(value)</code></td>
<td>지정된 최대값 이하여야 함</td>
<td>숫자</td>
</tr>
<tr>
<td><code>@DecimalMin(value)</code></td>
<td>지정된 실수 최소값 이상</td>
<td>실수, 문자열</td>
</tr>
<tr>
<td><code>@DecimalMax(value)</code></td>
<td>지정된 실수 최대값 이하</td>
<td>실수, 문자열</td>
</tr>
<tr>
<td><code>@Size(min, max)</code></td>
<td>문자열, 배열 등의 길이나 크기가 지정된 범위 내에 있어야 함</td>
<td>문자열, 컬렉션, 배열, Map</td>
</tr>
<tr>
<td><code>@Email</code></td>
<td>이메일 형식이어야 함</td>
<td>문자열</td>
</tr>
<tr>
<td><code>@Pattern(regexp)</code></td>
<td>정규식과 일치해야 함</td>
<td>문자열</td>
</tr>
<tr>
<td><code>@Past</code></td>
<td>과거 날짜여야 함</td>
<td>날짜 타입</td>
</tr>
<tr>
<td><code>@PastOrPresent</code></td>
<td>과거나 현재 날짜여야 함</td>
<td>날짜 타입</td>
</tr>
<tr>
<td><code>@Future</code></td>
<td>미래 날짜여야 함</td>
<td>날짜 타입</td>
</tr>
<tr>
<td><code>@FutureOrPresent</code></td>
<td>현재 또는 미래 날짜여야 함</td>
<td>날짜 타입</td>
</tr>
</tbody></table>
<h3 id="validate란">@Validate란?</h3>
<p><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/annotation/Validated.html">@Validated</a> 어노테이션은 org.springframework.validation 안에 있는 어노테이션이다. 
공식문서의 내용에 따르면 &quot;Variant of JSR-303&#39;s Valid, supporting the specification of validation groups&quot;,&quot;but also optionally specifying the validation groups for method-level validation in the annotated class.&quot;라고 한다. 즉, @Validated 어노테이션은 @Valid의 확장이며, &quot;Validation Group&quot;을 지원한다는 사실을 알 수 있다.
즉, @Validated는 @Valid와 비슷하지만 <strong>검증 그룹(Validation Group)</strong>을 사용할 수 있는 기능이 추가된 Spring 고유의 애노테이션이다.</p>
<h4 id="validation-group-사용법">Validation Group 사용법</h4>
<ol>
<li>유효성 검사 그룹을 식별하기 위한 “마커 인터페이스(Marker Interface) 생성.
참조 용도로만 사용되기 때문에 어떠한 동작을 실행하는 기능이 없는 빈 인터페이스여야 한다.<pre><code class="language-java">public class ValidationGroups {
 public interface SignUpGroup { }
 public interface UserUpdateGroup { }
</code></pre>
</li>
</ol>
<p>}</p>
<pre><code>2. DTO에서 그룹 적용
```java
public class SignUpDto {

    // 닉네임: 한글, 영문, 숫자 2~15자
    @NotBlank(groups = {ValidationGroups.SignUpGroup.class, ValidationGroups.UserUpdateGroup.class})
    @Pattern(
            regexp = &quot;^[가-힣a-zA-Z0-9]{2,15}$&quot;,
            message = &quot;닉네임은 한글, 영문, 숫자 조합 2~15자여야 합니다.&quot;,
            groups = {ValidationGroups.SignUpGroup.class, ValidationGroups.UserUpdateGroup.class}
    )
    }</code></pre><ol start="3">
<li>컨트롤러에서 그룹 유효성 검사<pre><code class="language-java">@PostMapping
 public String checkId(@RequestBody @Validated(ValidationGroups.SignUpGroup.class) SignUpDto signUpDto, Errors errors)</code></pre>
</li>
</ol>
<ol start="4">
<li>결과 확인
<img src="https://velog.velcdn.com/images/noah-wilson0/post/e1e247b6-8a5c-411c-b96e-c42a7d6b09b1/image.png" alt=""></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[지라 브랜치 기반으로 Cursor에서 Git 관리하기]]></title>
            <link>https://velog.io/@noah-wilson0/Cursor-JIRA-Git-%ED%98%91%EC%97%85-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5-%EC%A0%95%EB%A6%BD%EA%B8%B0</link>
            <guid>https://velog.io/@noah-wilson0/Cursor-JIRA-Git-%ED%98%91%EC%97%85-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5-%EC%A0%95%EB%A6%BD%EA%B8%B0</guid>
            <pubDate>Tue, 29 Apr 2025 16:42:55 GMT</pubDate>
            <description><![CDATA[<h2 id="cursor-ide에서-git사용의-불편함">CURSOR IDE에서 GIT사용의 불편함</h2>
<p>Cursor IDE는 VS Code 기반이긴 하지만, 기본 제공되는 Git GUI 기능에서는 커밋 템플릿을 불러오지 못하기 때문에 불편했다. 특히 커밋 메시지를 일정한 형식으로 작성하려는 경우에는 따로 지원되는 내장 기능이나 플러그인이 없어서 CLI를 함께 사용하는 게 훨씬 낫다는 결론에 도달했다.</p>
<p>그래서 다음과 같은 흐름으로 Git 브랜치 전략을 구성하고 사용하기로 했다:</p>
<ol>
<li><p>Jira 이슈 페이지에서 브랜치를 생성하면 원격 브랜치가 자동으로 만들어진다.</p>
</li>
<li><p>로컬에서는 동일한 이름의 브랜치를 수동으로 생성한다.</p>
</li>
<li><p>해당 로컬 브랜치를 원격 브랜치와 트래킹(tracking) 연결한 후 작업한다.</p>
<pre><code class="language-java">master branch (원격, 최종 통합 브랜치)
└─ feature/화면, 메인, 유저 등 (원격, 기능 단위 브랜치)
└─ local branch (로컬, 개발자 단위 브랜치)
</code></pre>
<p>결론적으로, Jira에서 기능 단위 원격 브랜치를 만들고, 로컬 브랜치에서 작업한 후 master로 병합하는 구조로 통일해나갈 계획이다. </p>
</li>
</ol>
<h2 id="remote-branch와-local-branch이란">REMOTE BRANCH와 LOCAL BRANCH이란?</h2>
<p>기존에는 IntelliJ 기반으로 Git을 사용했기 때문에, 새 브랜치를 만들면 자동으로 <strong>로컬 브랜치와 원격 브랜치가 같은 이름으로 연동되어 생성</strong>되는 경우가 많았다. 그래서 로컬과 원격 브랜치의 차이나 트래킹 개념을 정확히 인식하지 못한 채 작업해왔던 것이다.</p>
<p>그러나 Cursor IDE에서는 Git CLI 사용을 하게 되면서 <strong>로컬 브랜치 / 원격 브랜치 / 트래킹 브랜치의 개념을 명확하게 이해해야만</strong>  브랜치 관리가 가능했다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Remote Branch</strong></td>
<td>원격 저장소(GitHub 등)에 존재하는 브랜치. 예: <code>origin/master</code>, <code>origin/feature/INOUT-41-debate-room</code></td>
</tr>
<tr>
<td><strong>Local Branch</strong></td>
<td>내 PC에서만 존재하는 브랜치. 직접 작업하는 브랜치이며, 원격 브랜치와 연결(tracking)할 수 있음</td>
</tr>
<tr>
<td><strong>Tracking Branch</strong></td>
<td>로컬 브랜치가 원격 브랜치와 연결되어 있을 때, <code>git push</code>, <code>git pull</code>을 생략된 이름으로 자동 적용됨</td>
</tr>
</tbody></table>
<p> 로컬 브랜치가 원격 브랜치와 트래킹된 상태라면, git push만으로도 자동으로 연결된 브랜치로 푸시할 수 있다.</p>
<h2 id="cli에서-git-사용-해보기">CLI에서 GIT 사용 해보기</h2>
<h3 id="1-깃허브와-리액트-프로젝트-연동">1. 깃허브와 리액트 프로젝트 연동</h3>
<p>지라에서 원격 브랜치 생성 후</p>
<p>git checkout - b &quot;로컬브랜치&quot; &quot;원격 브랜치&quot; 원격 브랜치와 로컬 브랜치 트래킹 설정을 하였다.</p>
<h3 id="2git-commit-message-template적용">2.git commit message template적용</h3>
<p> git config --global commit.template &quot;커밋 메시지 템플릿 주소&quot;를 통해 커밋 메시지 템플릿을 적용했다.</p>
<h3 id="cli에서-최신정보-반영하는방법">CLI에서 최신정보 반영하는방법</h3>
<p>현재 이 상황은 다음과 같았다.</p>
<pre><code class="language-cli">origin/master       ← 최신 코드 있음
   ↑
   │   (반영하고 싶은 내용)
   ↓
INOUT-41-debate-stage  ← 작업 중인 로컬 브랜치 </code></pre>
<p>git checkout INOUT-41-debate-stage을 하여 내가 업데이트할 로컬 브랜치로 이동한다.</p>
<p>git fetch origin을 하여  원격 브랜치 최신 정보 가져온 후,
git merge origin/master를 통해 origin/master의 최신 내용을 병합한다.</p>
<p>성공했다면 아래 사진과 같은 흐름을 보이며 정상적으로 master 브랜치의 코드가 성공적으로 병합된다. 
<img src="https://velog.velcdn.com/images/noah-wilson0/post/ab19ffb5-db0b-4b60-98d9-96dcb3669f78/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[여행지 동기화 코드 성능 개선기: 빅오 분석과 Map 캐싱 최적화]]></title>
            <link>https://velog.io/@noah-wilson0/PlaceSyncService-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EA%B8%B0-%EB%B9%85%EC%98%A4-%EB%B6%84%EC%84%9D%EA%B3%BC-Map-%EC%BA%90%EC%8B%B1-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@noah-wilson0/PlaceSyncService-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EA%B8%B0-%EB%B9%85%EC%98%A4-%EB%B6%84%EC%84%9D%EA%B3%BC-Map-%EC%BA%90%EC%8B%B1-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Sun, 27 Apr 2025 13:00:45 GMT</pubDate>
            <description><![CDATA[<h2 id="1-기존-코드-문제점">1. 기존 코드 문제점</h2>
<p>기존의 여행지 동기화 코드는 tourSpotItems 리스트를 돌면서 다음과 같은 구조를 가지고 있었습니다:</p>
<pre><code class="language-java">for (PlaceResponse.PlaceItem placeItem : tourSpotItems) {
    // 1. contentId로 Place 조회
    Optional&lt;Place&gt; findPlace = placeService.findByContentId(placeItem.getContentid());

    if (findPlace.isPresent()) {
        // 2. Place가 존재하면, AreaCode와 CityCode를 조회하여 변경사항 비교
        AreaCode areaCode = areaCodeService.findByAreaCode(
            cityCodeService.findAreaCodeByCityCode(
                findPlace.get().getCityCode().getCityCode()
            ).orElseThrow(...)
        ).orElseThrow(...);

        // 3. 변경된 내용이 있으면 업데이트
        ...
    } else {
        // 4. Place가 존재하지 않으면 AreaCode와 CityCode를 조회하여 새로 저장
        AreaCode areaCode = areaCodeService.findByAreaCode(placeItem.getAreacode())
            .orElseThrow(...);
        CityCode cityCode = cityCodeService.findByCityCodeAndAreaCodeId(
            placeItem.getSigungucode(), areaCode.getAreaCodeId()
        ).orElseThrow(...);

        placeService.save(...);
    }
}

}</code></pre>
<h4 id="빅오big-o-분석">빅오(Big-O) 분석</h4>
<p>placeItems 개수가 N개라고 할 때,</p>
<p>매 placeItem마다 3번의 select 쿼리가 발생한다.</p>
<p>따라서 전체 쿼리 수는 <strong>O(N) x O(3N)</strong> 이다.</p>
<p>하지만, 쿼리당 네트워크 I/O 및 DB 접근 시간이 고정으로 존재하기 때문에,
결국 전체 처리 시간은 선형적으로 오래 걸리게 된다.</p>
<p>현실적인 문제</p>
<p>예를 들어 총 50000개의 데이터를 저장해야된다면,</p>
<p>총 3번 × 50000 = 약 15만 번의 DB 왕복 쿼리 발생</p>
<p>실제로 처리 시간이 32분이나 소요되었다...</p>
<h2 id="2-개선-방법">2. 개선 방법</h2>
<h4 id="개선-목표">개선 목표</h4>
<ul>
<li>DB 왕복 횟수를 극적으로 줄인다.</li>
<li>메모리에서 빠른 연산이 가능한 구조를 만든다.</li>
</ul>
<h4 id="개선-아이디어">개선 아이디어</h4>
<ul>
<li>Place, AreaCode, CityCode를 초기에 모두 한번에 조회해서 메모리에 올린다.</li>
<li>검색할 때마다 Map을 활용해 O(1) 시간복잡도로 빠르게 찾는다.</li>
</ul>
<p>개선된 코드 주요 구조</p>
<pre><code class="language-java">Map&lt;String, Place&gt; existingPlaces = placeService.findAllByContentIdIn(contentIds)
    .stream()
    .collect(Collectors.toMap(Place::getContentId, Function.identity()));

Map&lt;String, AreaCode&gt; areaCodeMap = areaCodeService.findAll()
    .stream()
    .collect(Collectors.toMap(AreaCode::getAreaCode, Function.identity()));

Map&lt;String, List&lt;CityCode&gt;&gt; cityCodeMap = cityCodeService.findAll()
    .stream()
    .collect(Collectors.groupingBy(cityCode -&gt; cityCode.getAreaCode().getAreaCode()));
</code></pre>
<p>이후 for문 구조</p>
<pre><code class="language-java">for (PlaceResponse.PlaceItem placeItem : tourSpotItems) {
    Place existingPlace = existingPlaces.get(placeItem.getContentid());
    AreaCode areaCode = areaCodeMap.get(placeItem.getAreacode());
    CityCode cityCode = cityCodeMap.getOrDefault(placeItem.getAreacode(), List.of())
        .stream()
        .filter(c -&gt; c.getCityCode().equals(placeItem.getSigungucode()))
        .findFirst()
        .orElse(null);
    // 이후 저장 또는 수정
}</code></pre>
<h4 id="빅오big-o-분석-1">빅오(Big-O) 분석</h4>
<p>초기에 Place, AreaCode, CityCode 조회: 각각 1번의 select (총 3번)</p>
<p>for문 순회는 순수 메모리 연산</p>
<p>Map 조회는 O(1) 시간복잡도
따라서 전체 쿼리 수는 O(N) + O(3N) 이다.</p>
<h4 id="최종-빅오">최종 빅오</h4>
<p>전체 처리 시간: O(N) (메모리 접근만)</p>
<p>DB 왕복 쿼리: O(3) (초기 한번씩만)</p>
<h2 id="3-결과">3. 결과</h2>
<p>기존: 32분 소요 (DB 왕복 15만 번)</p>
<p>개선: 약 2~3분 소요 (DB 왕복 3번)</p>
<h2 id="4-결론">4. 결론</h2>
<p>데이터량이 많은 경우, DB에서 자주 읽지 말고 미리 캐싱하자.</p>
<p>Map을 이용하면 O(1)로 빠르게 데이터 접근이 가능하다.</p>
<p>Stream과 Collectors를 이용해 초기 메모리 세팅을 깔끔하게 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ERD 개선(2)]]></title>
            <link>https://velog.io/@noah-wilson0/ERD-%EC%9E%AC%EC%84%A4%EA%B3%84-2</link>
            <guid>https://velog.io/@noah-wilson0/ERD-%EC%9E%AC%EC%84%A4%EA%B3%84-2</guid>
            <pubDate>Tue, 22 Apr 2025 12:40:16 GMT</pubDate>
            <description><![CDATA[<p>앞서 재설계한 ERD에서도 여전히 개선할 여지가 남아 있다.</p>
<ul>
<li>리뷰(review) 테이블에 추천 수를 나타내는 컬럼이 없어, 사용자 피드백을 수치화하거나 정렬 기준으로 활용하기 어렵다.</li>
<li>여행 경로 저장을 위한 테이블이 부재하여, 일정 간 장소 간 이동 경로를 명확히 표현하기 어렵다.</li>
<li>이동 수단을 저장하는 테이블이 존재하지 않아, 이동 방식에 따른 시간/거리 정보를 관리하기 어렵다.</li>
<li>여행지와 숙소가 각각 별도의 테이블로 분리되어 있어, 여행 경로를 from → to 형태로 저장할 때
장소 간 연결 정보를 설계하는 데 복잡함이 발생한다.</li>
</ul>
<h3 id="장소-테이블-통합으로-구조-간소화">장소 테이블 통합으로 구조 간소화</h3>
<p>이를 해결하기 위해, 여행지와 숙소를 하나의 place 테이블로 통합하고,place_type 컬럼을 도입하여 장소의 유형(숙소/관광지/음식점 등)을 구분하도록 구조를 변경하였다.</p>
<p>여행지, 숙소, 음식점 등을 하나의 place_id로 통일하여 처리할 수 있게 되었고,
장소 간 이동 경로를 저장하는 travel_plan_route에서는
from_place_id, to_place_id만으로 명확하게 경로를 표현할 수 있게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/f6413ef3-4ea9-4ca4-a47d-cfc64caf187f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[erd 설계 이슈]]></title>
            <link>https://velog.io/@noah-wilson0/erd-%EC%84%A4%EA%B3%84-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@noah-wilson0/erd-%EC%84%A4%EA%B3%84-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Tue, 22 Apr 2025 08:25:24 GMT</pubDate>
            <description><![CDATA[<h3 id="성급한-설계가-불러온-구조-변경의-결과">성급한 설계가 불러온 구조 변경의 결과</h3>
<p>개인 프로젝트를 시작하면서, 외부 API 데이터를 수집해 데이터베이스에 저장하기 위해 간단한 구조의 ERD를 먼저 설계하고, WebClient를 이용한 데이터 수집 및 저장 작업을 시작했습니다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/dd629d6f-e1d0-4ec6-88b3-0a911ada3d04/image.png" alt=""></p>
<p>초기에는 단순한 여행지 정보만 처리할 계획이었기 때문에, ERD도 최소한의 구조로 간단하게 설계하여 개발을 시작했습니다. 하지만 프로젝트를 진행하면서 기능을 추가하거나 수정하는 과정에서 전체적인 방향을 충분히 정리하지 않은 채 작업을 이어가다 보니, DB 구조가 계속 변경되었고,
여기에 DB 설계 역량이 부족했던 점까지 더해져 테이블을 반복적으로 수정하는 데 많은 시간을 소모하게 되었습니다.
그 결과, 기존 테이블 구조의 변경은 물론 테이블 간 관계 설정도 반복적으로 손봐야 하는 상황이 발생하게 되었습니다.</p>
<p>특히, 외부 API의 데이터 구조를 충분히 분석하지 않고 개발을 서둘렀던 점이 이후 구조 변경의 가장 큰 원인이 되었다.</p>
<h3 id="erd-재설계">ERD 재설계</h3>
<p>DB 구조를 어차피 전반적으로 수정해야 하는 상황이었기 때문에, 기존 ERD의 문제점부터 파악해보았습니다.
처음에는 외부 API에서 제공하는 식별값(content_id 등)을 PK로 사용하면 안 된다는 개념에 익숙하지 않아, 중간에 대체 키(id)를 도입하긴 했지만, 설계에 일관성이 없어서 오히려 불필요하거나 애매한 구조가 되었습니다.</p>
<p>이에 따라 ERD를 전면적으로 수정하며 다음과 같은 원칙을 적용했습니다:</p>
<ul>
<li>모든 테이블에 내부 식별용 id를 AUTO INCREMENT로 생성하여 기본키(PK)로 사용</li>
<li>외부 API가 제공하는 식별값(content_id 등)은 비즈니스 키로 보고 UNIQUE 제약조건만 부여</li>
</ul>
<p>이러한 구조로 변경함으로써, 외래 키(FK)가 비즈니스 키의 변경에 영향을 받는 문제를 예방할 수 있었고, 데이터의 정합성과 유지보수성도 함께 개선할 수 있었습니다.</p>
<h3 id="개선된-erd">개선된 ERD</h3>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/fe4be204-9162-450d-81fa-5e09b60eff04/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[github+jira 연동]]></title>
            <link>https://velog.io/@noah-wilson0/githubjira-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@noah-wilson0/githubjira-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Sun, 06 Apr 2025 14:17:03 GMT</pubDate>
            <description><![CDATA[<h2 id="github-jira-연동하기">github jira 연동하기</h2>
<ol>
<li>지라 로그인 - 앱 - 더 많은 앱 살펴보기
<img src="https://velog.velcdn.com/images/noah-wilson0/post/fa98fc50-f9a0-448b-96db-754022eb6a34/image.png" alt=""></li>
</ol>
<p>2.검색창에 github for jira 검색 후 클릭
<img src="https://velog.velcdn.com/images/noah-wilson0/post/d9a225af-32e6-40ab-947b-794cb089a6a5/image.png" alt=""></p>
<ol start="3">
<li>getapp을 하여 Atlassian과 github 연동하기</li>
</ol>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/f559b96d-568e-4e7f-ac0b-8df1c363ed17/image.png" alt=""></p>
<ol start="4">
<li>add another organization - Select an organization in Git-Hub을 클릭 후 open할 repo를 선택한다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/2440b435-35d6-4183-9825-f8ad0a888a7d/image.png" alt=""></li>
<li>jira github 연동 결과
<img src="https://velog.velcdn.com/images/noah-wilson0/post/13a6f57e-ba64-4aa8-9677-d5c2c33e7688/image.png" alt=""></li>
</ol>
<h2 id="jira-이슈와-github-브랜치-연동">Jira 이슈와 GitHub 브랜치 연동</h2>
<p><a href="https://roll-over-program.tistory.com/78">https://roll-over-program.tistory.com/78</a>
위 블로그 참고하여 branch연동</p>
<p>이슈키를 포함하여 branch를 생성하고 git commit message에 이슈키를 포함하여 commit을 해야 jira에서 
이슈 추적이 자동으로 된다
그러므로 git commit message template을 수정해야할듯</p>
<h2 id="github-action로--jira-issue-자동화하기">github action로  jira issue 자동화하기</h2>
<p>참고 블로그: <a href="https://velog.io/@sangpok/Github-%ED%98%91%EC%97%85-%EC%84%A4%EC%A0%95Github-Issue-Jira-%EC%97%B0%EB%8F%99">https://velog.io/@sangpok/Github-%ED%98%91%EC%97%85-%EC%84%A4%EC%A0%95Github-Issue-Jira-%EC%97%B0%EB%8F%99</a></p>
<h3 id="1-github-repository-secret-등록하기">1. github repository secret 등록하기</h3>
<ol>
<li>secrets를 등록하고 싶은 github repo에 접속한 뒤에, &quot;Settings&gt;Security&gt;Secrets and variables &gt;Actions&quot; 탭에 접근해 나오는 페이지에서 하단 &quot;New repository secret&quot;을 누른다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/a3f976da-0779-4d88-b4ed-5e683a4bdea8/image.png" alt=""></li>
</ol>
<p>Name에는 변수명을, Secret에는 값을 입력하고 &quot;add secret&quot;을 누른다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/b90128d8-a2b4-435b-980d-76bef7006222/image.png" alt=""></p>
<p>GitHub에 저장된 Secrets 값은 워크플로우 파일 안에서 ${{ secrets.VARIABLE_NAME }} 형태로 불러올 수 있다.
예를 들어, Jira 로그인을 위해 아래와 같이 환경 변수를 설정하면 된다:</p>
<pre><code class="language-yaml">env:
  JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
  JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
  JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
</code></pre>
<h3 id="2-githubissue_template-작성">2. .github/ISSUE_TEMPLATE 작성</h3>
<pre><code class="language-yml">name: &#39;이슈 생성&#39;
description: &#39;Repo에 이슈를 생성하며, 생성된 이슈는 Jira와 연동됩니다.&#39;
labels: [feat]
title: &#39;이슈 이름을 작성해주세요&#39;
body:
  - type: input
    id: parentKey
    attributes:
      label: &#39;🎟️ 상위 작업 (Ticket Number)&#39;
      description: &#39;상위 작업의 Ticket Number를 기입해주세요&#39;
      placeholder: &#39;INOUT-00&#39;
    validations:
      required: true

  - type: input
    id: branch
    attributes:
      label: &#39;🌳 브랜치명 (Branch)&#39;
      description: &#39;영어로 브랜치명을 작성해주세요&#39;
    validations:
      required: true

  - type: input
    id: description
    attributes:
      label: &#39;📝 상세 내용(Description)&#39;
      description: &#39;이슈에 대해서 간략히 설명해주세요&#39;
    validations:
      required: true

  - type: textarea
    id: tasks
    attributes:
      label: &#39;✅ 체크리스트(Tasks)&#39;
      description: &#39;해당 이슈에 대해 필요한 작업목록을 작성해주세요&#39;
      value: |
        - [ ] Task1
        - [ ] Task2
    validations:
      required: true</code></pre>
<h3 id="3-githubworkflowscreate-jira-issyeyml-작성">3. .github/workflows/create-jira-issye.yml 작성</h3>
<pre><code class="language-yml">name: Create Jira issue
on:
  issues:
    types:
      - opened
jobs:
  create-issue:
    name: Create Jira issue
    runs-on: ubuntu-latest
    steps:
      - name: Login
        uses: atlassian/gajira-login@v3
        env:
          JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
          JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
          JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}

      - name: Checkout main code
        uses: actions/checkout@v4
        with:
          ref: main

      - name: Issue Parser
        uses: stefanbuck/github-issue-praser@v3
        id: issue-parser
        with:
          template-path: .github/ISSUE_TEMPLATE/issue-form.yml

      - name: Log Issue Parser
        run: |
          echo &#39;${{ steps.issue-parser.outputs.issueparser_parentKey }}&#39;
          echo &#39;${{ steps.issue-parser.outputs.__ticket_number }}&#39;
          echo &#39;${{ steps.issue-parser.outputs.jsonString }}&#39;

      - name: Convert markdown to Jira Syntax
        uses: peter-evans/jira2md@v1
        id: md2jira
        with:
          input-text: |
            ### Github Issue Link
            - ${{ github.event.issue.html_url }}

            ${{ github.event.issue.body }}
          mode: md2jira

      - name: Create Issue
        id: create
        uses: atlassian/gajira-create@v3
        with:
          project: SKP
          issuetype: Task
          summary: &#39;${{ github.event.issue.title }}&#39;
          description: &#39;${{ steps.md2jira.outputs.output-text }}&#39;
          fields: |
            {
              &quot;parent&quot;: {
                &quot;key&quot;: &quot;${{ steps.issue-parser.outputs.issueparser_parentKey }}&quot;
              }
            }

      - name: Log created issue
        run: echo &quot;Jira Issue ${{ steps.issue-parser.outputs.parentKey }}/${{ steps.create.outputs.issue }} was created&quot;

      - name: Checkout develop code
        uses: actions/checkout@v4
        with:
          ref: develop

      - name: Create branch with Ticket number
        run: |
          ISSUE_NUMBER=&quot;${{ steps.create.outputs.issue }}&quot;
          ISSUE_TITLE=&quot;${{ steps.issue-parser.outputs.issueparser_branch}}&quot;
          BRANCH_NAME=&quot;${ISSUE_NUMBER}-$(echo ${ISSUE_TITLE} | sed &#39;s/ /-/g&#39;)&quot;
          git checkout -b &quot;${BRANCH_NAME}&quot;
          git push origin &quot;${BRANCH_NAME}&quot;

      - name: Update issue title
        uses: actions-cool/issues-helper@v3
        with:
          actions: &#39;update-issue&#39;
          token: ${{ secrets.GITHUB_TOKEN }}
          title: &#39;[${{ steps.create.outputs.issue }}] ${{ github.event.issue.title }}&#39;

      - name: Add comment with Jira issue link
        uses: actions-cool/issues-helper@v3
        with:
          actions: &#39;create-comment&#39;
          token: ${{ secrets.GITHUB_TOKEN }}
          issue-number: ${{ github.event.issue.number }}
          body: &#39;Jira Issue Created: [${{ steps.create.outputs.issue }}](${{ secrets.JIRA_BASE_URL }}/browse/${{ steps.create.outputs.issue }})&#39;
</code></pre>
<p>issue가 close될떄도 처리하기 위한 close-jira-issue.yml도 추가했다.</p>
<pre><code class="language-yml">name: Close Jira issue
on:
  issues:
    types:
      - closed

jobs:
  close-issue:
    name: Close Jira issue
    runs-on: ubuntu-latest

    steps:
      - name: Login to Jira
        uses: atlassian/gajira-login@v3
        env:
          JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
          JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
          JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}

      - name: Extract Jira issue key from GitHub issue title
        id: extract-key
        run: |
          ISSUE_TITLE=&quot;${{ github.event.issue.title }}&quot;
          JIRA_KEY=$(echo &quot;$ISSUE_TITLE&quot; | grep -oE &#39;[A-Z]+-[0-9]+&#39;)
          echo &quot;JIRA_KEY=$JIRA_KEY&quot; &gt;&gt; $GITHUB_ENV

      - name: Close Jira issue
        if: env.JIRA_KEY != &#39;&#39;
        uses: atlassian/gajira-transition@v3
        with:
          issue: ${{ env.JIRA_KEY }}
          transition: Done</code></pre>
<h3 id="4-github-action-권한-설정">4. Github Action 권한 설정</h3>
<p>settings - Actions -General탭에 접근하여 
 Read and Write Permission 체크하기
<img src="https://velog.velcdn.com/images/noah-wilson0/post/ec107d76-2cdc-49cf-82f3-25c553681e06/image.png" alt=""></p>
<h3 id="5-github-actions-테스트">5. github Actions 테스트</h3>
<p>먼저 GitHub의 Issues 탭에 들어가서 새로운 이슈를 작성해준다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/7e84e05d-789e-4b96-a45d-d882a2367fcc/image.png" alt=""></p>
<p>이슈를 생성하면, 자동으로 실행되는 GitHub Actions를 Actions 탭에서 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/f1755f00-b840-4ee0-bda3-80a287f2cd92/image.png" alt=""></p>
<p>정상적으로 작동했다면, 아래와 같이 Jira에 자동으로 이슈가 생성된 것도 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/a134b162-01c5-4c42-a46d-fb8b117f8e06/image.png" alt=""></p>
<blockquote>
<p>원래는 생성된 Jira 이슈 상태에 따라 작업 브랜치를 자동으로 상위 브랜치에 병합되도록 GitHub Actions로 자동화를 시도했었다.
예를 들어, Sub-task는 Task 브랜치로, Task는 Epic 브랜치로, 그리고 최종적으로 Epic은 master 브랜치로 병합되도록 하는 구조였는데,
아직 Git 사용에 익숙하지 않다 보니 현재는 브랜치 병합은 수동으로 진행하고 있다.
추후 Git 사용에 익숙해지면 이 부분도 다시 자동화할 예정!</p>
</blockquote>
<h2 id="sub-task-생성-삽질기">Sub-task 생성 삽질기</h2>
<p>위 글의 .github/workflows/create-jira-issye.yml를 보면 완성된 코드가 이미 들어가 있지만 
task(모든 기타 표준 이슈 유형) 유형 이슈에서 Sub-task(모든 하위 작업 이슈 유형) 유형 이슈를  연동해주는 과정에서 발생한 에러에 대한 내용이다.</p>
<p>우선 jira의 이슈 계층 구조는 다음과 같다.</p>
<ol>
<li><p>Epic
가장 상위 계층 (hierarchyLevel: -1)
하위로는 표준 이슈 유형들만 가능 (예: Task, Story, Bug 등)
❌ Sub-task는 직접적으로 Epic 밑에 둘 수 없음</p>
</li>
<li><p>Standard Issue Types (모든 기타 표준 이슈 유형)(hierarchyLevel: 0)
예: Task, Story, Bug 등
상위: Epic 가능 ✅
하위: Sub-task 가능 ✅</p>
</li>
<li><p>Sub-task(모든 하위 작업 이슈 유형) (hierarchyLevel: -1)
반드시 표준 이슈 유형(Task, Story 등)의 하위로만 생성 가능
❌ Epic의 직접 하위가 될 수 없음</p>
</li>
</ol>
<p>위 구조는 기본적으로 jira free tier에서 고정된 계층 구조이다. 그러므로 당연히 Sub-task가 Task와 연동 될 줄 알았다. issue를 만들어보면 다음과 같은 에러가 생긴다.
<img src="https://velog.velcdn.com/images/noah-wilson0/post/fb3def9d-b117-41bc-b0c1-73509fb4ed9e/image.png" alt=""></p>
<p>관리자 설정과 프로젝트 설정 모두 &#39;Sub-task&#39;를 설정해주어도 오류가 해결되지 않았지만 프로젝트 설정&gt; 하위작업을 &#39;Sub-task&#39;로 통일하니깐 해결되었다(모든 name을 통일해 줘야 하는듯 왠래 &#39;하위 작업&#39;으로 표시되어 있었는데 같은 의미인줄 알고 내버려 둿었다.)</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/36361bbd-33c6-4630-a602-56fd00d904b7/image.png" alt=""></p>
<p>변경 후 실행 결과:
<img src="https://velog.velcdn.com/images/noah-wilson0/post/d05382ba-b50c-4b03-8d5f-e68ea41a85b0/image.png" alt=""></p>
<h3 id="참고">참고</h3>
<h4 id="1설정되지-않은-issuetype을-설정하면-이렇게-오류가-생긴다">1.설정되지 않은 issueType을 설정하면 이렇게 오류가 생긴다.</h4>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/6150dd81-8acf-4a7f-bf06-d0e360101b77/image.png" alt=""></p>
<h3 id="2-존재하지-않는-branch를설정하면-생기는-오류">2. 존재하지 않는 branch를설정하면 생기는 오류</h3>
<pre><code class="language-yml">      - name: Checkout jiratest code
        uses: actions/checkout@v4
        with:
          ref: 존재하지 않는 branch</code></pre>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/c8f48d42-acf7-4434-9543-e556fbca0ceb/image.png" alt=""></p>
<pre><code class="language-yml">      - name: Checkout jiratest code
        uses: actions/checkout@v4
        with:
          ref: main</code></pre>
<p>존재하는 branch로 수정하면 해결된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[jira 사용해보기]]></title>
            <link>https://velog.io/@noah-wilson0/jira-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@noah-wilson0/jira-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 06 Apr 2025 13:36:30 GMT</pubDate>
            <description><![CDATA[<h2 id="협업-툴을-사용해야-되는-이유">협업 툴을 사용해야 되는 이유</h2>
<p>예전에 팀 프로젝트를 진행할 땐 개인적으로 기획서는 문서로, 일정은 캘린더로, 소통은 디스코드, 카카오톡을 같이 사용했었다.
처음엔 괜찮았지만, 시간이 지날수록 업무 메시지랑 개인 메시지가 섞이고, 회의 내용은 디스코드에 정리했지만 결국 기억에 의존하거나 코드 주석으로 메모하는 식으로 진행하게 됐었다.</p>
<p>근데 그렇게 하다 보니 전체적인 흐름을 파악하기가 점점 어려워졌고,회의 내용도 정해진 템플릿 없이 
그때그때 대충 메모하는 식으로 작성되다 보니
정작 나중에 회의록을 봐도 무슨 얘길 했는지 잘 기억이 안 났다.</p>
<p>예를 들어 &quot;이번 주에 누구가 로그인 기능 맡기로 했음&quot; 이런 식으로만 적어두고,
구체적인 마감 기한이나 어떤 방식으로 구현할지 같은 중요한 내용은 빠지는 경우가 많았다.
그러다 보니 일정이 밀리거나, 서로 다른 방식으로 작업해서 충돌이 생기는 경우도 종종 있었다.
그래서 이번 프로젝트에선 처음부터 Jira를 도입해보기로 했다.</p>
<p>협업 툴이 워낙 많긴 하지만, Jira는 프로젝트의 흐름을 타임라인으로 추적할 수 있고,
작업 보드에서 각자의 할 일과 진행 상황을 한눈에 파악할 수 있어서 꽤 만족스러웠다.
특히 팀원 간 업무가 어떻게 분배되어 있는지도 명확하게 보여서, 소통할 때 훨씬 수월해졌다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git Commit Message Template 사용해보기 ]]></title>
            <link>https://velog.io/@noah-wilson0/Git-Commit-Message-Template-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@noah-wilson0/Git-Commit-Message-Template-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 25 Mar 2025 12:52:17 GMT</pubDate>
            <description><![CDATA[<h3 id="왜-커밋-메시지를-잘-써야-할까">왜 커밋 메시지를 잘 써야 할까?</h3>
<p>새로운 개인 프로젝트를 시작하면서, 이전 프로젝트들에서의 Git 커밋 메시지 작성 습관을 되돌아보게 되었다. 그동안은 기능을 구현할 때마다 메시지를 간단하게 &quot;xx 기능 구현 완료&quot;라고 작성하거나, 때로는 아예 커밋 메시지를 생략하기도 했다.</p>
<p>이러한 방식은 처음에는 편하게 느껴졌지만, 나중에 해당 코드를 수정하거나 회귀할 때 큰 불편함이 있었다. 과거의 커밋 메시지를 확인해봐도 어떤 작업을 했는지 명확하게 파악하기 어려워 결국 코드를 다시 처음부터 읽고, 그때의 의도를 기억해내려 애써야 했기 때문이다.</p>
<p>그래서 이번에는 커밋 메시지를 좀 더 체계적으로 관리하기로 마음먹었다. <a href="https://meetup.nhncloud.com/posts/106">잘 정리된 블로그 글</a>을 참고하면서, Git 커밋 메시지 템플릿을 활용해 커밋 기록을 명확하고 일관성 있게 남기려 한다.</p>
<h3 id="commit-message-7가지-규칙">Commit Message 7가지 규칙</h3>
<ol>
<li>제목과 본문을 한 행으로 구분</li>
<li>제목은 영문 기준 50자 이내 작성</li>
<li>제목 첫글자는 대문자로 작성</li>
<li>제목 끝에 마침표(.) 금지</li>
<li>제목은 명령문으로 작성</li>
<li>본문의 각 행은 72자 내로 작성</li>
<li>본문은 어떻게보다 무엇을, 왜에 맞춰 작성</li>
</ol>
<h3 id="커밋-메시지-기본구조">커밋 메시지 기본구조</h3>
<p>커밋 메세지의 구조는 공식 가이드라은 없지만 기본적으로  Subject, Body, Footer세 부분으로 나뉘며, 각 부분을 한 줄의 공백으로 구분한다.</p>
<pre><code>type :  Subject

body

footer</code></pre><h4 id="git-commit-type-명세표">Git Commit Type 명세표</h4>
<table>
<thead>
<tr>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>ADD</code></td>
<td>새로운 코드나 파일을 추가할 때 사용</td>
</tr>
<tr>
<td><code>MODIFY</code></td>
<td>기존 기능의 일부를 수정할 때 사용</td>
</tr>
<tr>
<td><code>FIX</code></td>
<td>버그를 수정했을 때 사용</td>
</tr>
<tr>
<td><code>DOCS</code></td>
<td>README, 주석 등 문서 관련 수정 시 사용</td>
</tr>
<tr>
<td><code>REMOVE</code></td>
<td>코드나 파일을 삭제할 때 사용</td>
</tr>
<tr>
<td><code>IMPLEMENT</code></td>
<td>무언가의 전체 동작을 구현했을 때 사용. <code>ADD</code>보다 큰 기능 단위에 사용</td>
</tr>
<tr>
<td><code>TEST</code></td>
<td>테스트 코드 추가 및 수정 시 사용</td>
</tr>
<tr>
<td><code>RENAME</code></td>
<td>파일명 변경, 변수명, 함수명 등 이름만 변경했을 때 사용</td>
</tr>
<tr>
<td><code>MOVE</code></td>
<td>코드나 파일을 디렉토리 간 이동했을 때 사용</td>
</tr>
<tr>
<td><code>CHORE</code></td>
<td>빌드 설정, 패키지 매니저 설정 등 기타 잡다한 작업에 사용</td>
</tr>
</tbody></table>
<h4 id="내가-사용할-git-commit-message-template">내가 사용할 Git Commit Message Template</h4>
<pre><code># type: Subject

# 본문

# ----------------------------------------
#  Type 종류
# ----------------------------------------
# ADD       : 새로운 코드나 파일 추가
# MODIFY    : 기존 기능 수정
# FIX       : 버그 수정
# DOCS      : 문서(주석, README 등) 수정
# REMOVE    : 코드나 파일 삭제
# IMPLEMENT : 무언가의 전체 동작을 &quot;구현&quot;했을 때 사용 (ADD보다 큰 단위)
# TEST      : 테스트 코드 추가 및 수정
# RENAME    : 파일명, 변수명, 함수명 등의 이름 변경
# MOVE      : 코드나 파일 이동
# CHORE     : 빌드/패키지 매니저 설정 등 기타 작업

# ----------------------------------------
#  제목(Subject) 작성 규칙
# ----------------------------------------
# - 제목 작성 후 반드시 한 줄 띄워서 본문과 구분
# - 제목은 대문자로 시작, 마침표는 쓰지 않음
# - 제목은 명령문으로 작성 

# ----------------------------------------
#  본문(Body) 작성 규칙
# ----------------------------------------
# - 본문 작성 후 한 줄 띄워서 Resolves 영역과 구분
# - 각 행은 72자 이내로 작성
# - &quot;무엇을&quot;, &quot;왜&quot; 했는지를 중심으로 작성
# - 본문은 여러 줄로 구성 가능</code></pre><h3 id="git-commit-message-template-적용하기">git commit message template 적용하기</h3>
<p>ntelliJ - File - Settings - Plugin에 가서
commit message template을 사용하여 적용하였다.
 <img src="https://velog.velcdn.com/images/noah-wilson0/post/37f156e1-23bb-49e2-8d6e-734756bf4c33/image.png" alt=""></p>
<p> 위 플러그인은 설치후 적용 결과 한글이 깨지는 현상이 생겻다..</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/bd5b1b92-499f-4794-9beb-69f04f813c9f/image.png" alt=""></p>
<p>  상단 가장 우측 Help - Edit Custom VM Options 에서  -Dfile.encoding=UTF-8을 추가해서 해결 완료 </p>
<p> <img src="https://velog.velcdn.com/images/noah-wilson0/post/3e8c2e7e-adca-44ea-93b1-f4723d428e4a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] 영속성 전이: CASCADE]]></title>
            <link>https://velog.io/@noah-wilson0/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%A0%84%EC%9D%B4-CASCADE</link>
            <guid>https://velog.io/@noah-wilson0/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%A0%84%EC%9D%B4-CASCADE</guid>
            <pubDate>Thu, 09 Jan 2025 12:06:21 GMT</pubDate>
            <description><![CDATA[<h3 id="영속성-전이cascade-란">영속성 전이(CASCADE) 란?</h3>
<p>특정 엔티티를 영속 상태로 만들 떄 연관되어 있는 엔티티도 함께 영속 상태로 만드는것이다.</p>
<p>만약 Parent 객체와 child 객체가 연관되어 있다고 가정하자.</p>
<pre><code class="language-java">@Entity
public class Parent {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = &quot;parent&quot;)
    private List&lt;Child&gt; childList=new ArrayList&lt;Child&gt;();
}</code></pre>
<pre><code class="language-java">@Entity
public class Child {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = &quot;parent_id&quot;)
    private Parent parent;
}</code></pre>
<pre><code class="language-java">            Child child1 = new Child();
            Child child2 = new Child();

            Parent parent = new Parent();
            parent.addChild(child1);
            parent.addChild(child2);

            em.persist(parent);
            em.persist(child1);
            em.persist(child2);</code></pre>
<p>child객체의 갯수만큼 계속 persist하면 너무 귀찮다!
이때 사용하는 것이 cascade다.</p>
<pre><code class="language-java">@Entity
public class Parent {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = &quot;parent&quot;,cascade = CascadeType.ALL)
    private List&lt;Child&gt; childList=new ArrayList&lt;Child&gt;();

}</code></pre>
<p>child 코드는 동일하다.</p>
<pre><code class="language-java">            Child child1 = new Child();
            Child child2 = new Child();

            Parent parent = new Parent();
            parent.addChild(child1);
            parent.addChild(child2);

            em.persist(parent);</code></pre>
<p>실행 결과:</p>
<pre><code class="language-log">Hibernate: 
    /* insert for
        hellojpa.Parent */insert 
    into
        Parent (name, id) 
    values
        (?, ?)
Hibernate: 
    /* insert for
        hellojpa.Child */insert 
    into
        Child (name, parent_id, id) 
    values
        (?, ?, ?)
Hibernate: 
    /* insert for
        hellojpa.Child */insert 
    into
        Child (name, parent_id, id) 
    values
        (?, ?, ?)</code></pre>
<p>Parent에 cascade = CascadeType.ALL을 설정해줌으로 Parent객체만 persist해도 자식객체가 전부 저장된다.</p>
<p>헷갈릴수도 있지만 영속성 전이(cascade)는 연관관계를 매핑하는 것과 아무 관련이 없다. 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함
을 제공할 뿐이다.</p>
<h4 id="주의사항">주의사항</h4>
<p><span style="color:red"> 참조하는 곳이 하나일 때 사용해야한다.</span>(자식 엔티티가 서로 다른 2개 이상의 부모 엔티티가 연관관계를 갖고있다면 조심해야 한다.)</p>
<p>참고로 CASCADE의 종류는 All, persist, remove, merge, refresh, detach가 있다.</p>
<h3 id="고아객체란">고아객체란?</h3>
<p>고아 객체는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 말한다.</p>
<h4 id="orphanremoval--true-옵션">orphanRemoval = true 옵션</h4>
<p>참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능</p>
<ul>
<li>@OneToOne, @OneToMany만 가능</li>
<li><span style="color:red"> 참조하는 곳이 하나일 때 사용해야한다.</span>(자식 엔티티가 서로 다른 2개 이상의 부모 엔티티가 연관관계를 갖고있다면 조심해야 한다.)</li>
<li>특정 엔티티가 개인 소유할 때 사용</li>
</ul>
<p>참고 자료 출처: <a href="https://www.inflearn.com/course/ORM-JPA-Basic">출처</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] 프록시]]></title>
            <link>https://velog.io/@noah-wilson0/JPA-%ED%94%84%EB%A1%9D%EC%8B%9C</link>
            <guid>https://velog.io/@noah-wilson0/JPA-%ED%94%84%EB%A1%9D%EC%8B%9C</guid>
            <pubDate>Thu, 09 Jan 2025 09:52:01 GMT</pubDate>
            <description><![CDATA[<h3 id="프록시proxy란">프록시(Proxy)란?</h3>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/c087c51f-a042-48b8-95cc-6bcac419cb96/image.png" alt=""></p>
<p>프록시(Proxy)란, 영문 그대로 &#39;대리자&#39;라는 뜻으로, 실제 객체를 대신하여 대리 역할을 수행하는 객체를 말한다. </p>
<p>위 사진과 같이 프록시는 실제 클래스를 상속받아서 만들어지기떄문에 실제 클래스 똑같이 생겼다.
이론상 사용자 관점에서는 진짜 객체인지 프록시인지 구분하지 않고 사용할 수 있다.</p>
<h4 id="프록시의-동작-과정">프록시의 동작 과정</h4>
<p>프록시 객체는 실제 객체의 참조(target)을 가지고 있으므로 프록시 객체를 호출하면 실제 객체의 메소드를 호출한다.</p>
<p>em.getReference()를 하여 프록시 객체를 생성할 수 있다.</p>
<p>다음 그림은 em.getReference()을 통해 MemberProxy를 생성후 .getName()을 호출했을떄 동작 과정을 나타낸 그림이다.(영속성 컨텍스트가 클린(clean)한 상태 가정)</p>
<p><img src="https://velog.velcdn.com/images/noah-wilson0/post/870852d6-4865-4381-80b2-b4d3d070a448/image.png" alt=""></p>
<ol>
<li>getName() 호출</li>
<li>프록시(MemberProxy)는  영속성 컨텍스트에 초기화 요청을 한다. </li>
<li>member를 영속화하기 위해 DB 조회 </li>
<li>실제 member 객체 영속화 후 프록시(MemberProxy)의 참조(target)과 연결한다.</li>
<li>프록시(MemberProxy)가 참조(target)의 메소드(getName)을 호출후 반환한다.</li>
</ol>
<span style="color:gray"> 
+ 참고 <br>
em.find()는 데이터 베이스를 통해서 실제 엔티티 객체 조회한다. <br>
em.getReference()는 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회한다.
</span>


<h4 id="정리">정리</h4>
<ul>
<li><p>프록시 객체는 처음 사용할 때 한 번만 초기화된다.</p>
</li>
<li><p>프록시를 초기화한다는 것은 프록시 객체가 실제 엔티티로 바뀌는것이 아니다.</p>
</li>
</ul>
<pre><code class="language-java">            Member findMember = em.getReference(Member.class, member.getId());
            System.out.println(&quot;findMember.getClass() = &quot; + findMember.getClass());

            findMember.getUsername();

            System.out.println(&quot;findMember.getClass() = &quot; + findMember.getClass());</code></pre>
<p>실행 결과:</p>
<pre><code class="language-log">findMember.getClass() = class hellojpa.Member$HibernateProxy$NXbKWHsJ
Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.createBy,
        m1_0.createdDate,
        m1_0.lastModifiedBy,
        m1_0.lastModifiedDate,
        t1_0.TEAM_ID,
        t1_0.name,
        m1_0.USERNAME 
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.team_TEAM_ID 
    where
        m1_0.MEMBER_ID=?
findMember.getClass() = class hellojpa.Member$HibernateProxy$NXbKWHsJ</code></pre>
<ul>
<li><p>프록시 객체는 원본 엔티티를 상속 받으므로 
<span style="color:red"> 타입체크 </span>를 주의해야 한다. 타입 체크시 == 비교를 사용하지 않고 instance of를 사용하자.</p>
<pre><code class="language-java">
          Member RefMember = em.getReference(Member.class, member.getId());
         System.out.println(&quot;RefMember.getClass() = &quot; + RefMember.getClass());
         System.out.println(&quot; RefMember instanceof Member = &quot; + (RefMember instanceof Member));</code></pre>
<p>실행결과:</p>
<pre><code class="language-log">RefMember.getClass() = class hellojpa.Member$HibernateProxy$uyIJHorZ
RefMember instanceof Member = true</code></pre>
</li>
</ul>
<ul>
<li>영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해 도 실제 엔티티 반환된다.</li>
</ul>
<pre><code class="language-java">            Member findMember = em.find(Member.class, member.getId());
            System.out.println(&quot;findMember.getClass() = &quot; + findMember.getClass());

            findMember.getUsername();
            Member RefMember = em.getReference(Member.class, member.getId());
            System.out.println(&quot;RefMember.getClass() = &quot; + RefMember.getClass());</code></pre>
<p>실행 결과:</p>
<pre><code class="language-log">findMember.getClass() = class hellojpa.Member
findMember.getClass() = class hellojpa.Member</code></pre>
<ul>
<li>==비교가 필요할 경우를 대비하여  jpa는 항상 한 트렌잭션 안 에서 같은 엔티티에 대한 동일성을 보장을 해준다.(==는 항상 true여야 한다.)</li>
</ul>
<pre><code class="language-java">            System.out.println(&quot;================&quot;);
            Member findMember = em.find(Member.class, member.getId());
            System.out.println(&quot;findMember.getClass() = &quot; + findMember.getClass());

            Member RefMember = em.getReference(Member.class, member.getId());
            System.out.println(&quot;RefMember.getClass() = &quot; + RefMember.getClass());

            em.clear();
            System.out.println(&quot;================&quot;);
            Member RefMember2 = em.getReference(Member.class, member.getId());
            System.out.println(&quot;RefMember2.getClass() = &quot; + RefMember2.getClass());

            Member findMember2 = em.find(Member.class, member.getId());
            System.out.println(&quot;findMember2.getClass() = &quot; + findMember2.getClass());

            System.out.println(&quot;findMember == RefMember:&quot; + (findMember==RefMember));
            System.out.println(&quot;findMember2 == RefMember2:&quot; + (findMember2==RefMember2));</code></pre>
<p>실행 결과:</p>
<pre><code class="language-log">================
Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.createBy,
        m1_0.createdDate,
        m1_0.lastModifiedBy,
        m1_0.lastModifiedDate,
        t1_0.TEAM_ID,
        t1_0.name,
        m1_0.USERNAME 
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.team_TEAM_ID 
    where
        m1_0.MEMBER_ID=?
findMember.getClass() = class hellojpa.Member
RefMember.getClass() = class hellojpa.Member
================
RefMember2.getClass() = class hellojpa.Member$HibernateProxy$dfy1PGsC
Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.createBy,
        m1_0.createdDate,
        m1_0.lastModifiedBy,
        m1_0.lastModifiedDate,
        t1_0.TEAM_ID,
        t1_0.name,
        m1_0.USERNAME 
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.team_TEAM_ID 
    where
        m1_0.MEMBER_ID=?
findMember2.getClass() = class hellojpa.Member$HibernateProxy$dfy1PGsC
================
findMember == RefMember:true
findMember2 == RefMember2:true</code></pre>
<ul>
<li>영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면문제 발생
(하이버네이트는org.hibernate.LazyInitializationException 예외를 터트림)</li>
</ul>
<pre><code class="language-java">            Member refMember = em.getReference(Member.class, member.getId());

            em.detach(refMember);

            System.out.println(&quot;refMember.getUsername() = &quot; + refMember.getUsername());</code></pre>
<pre><code class="language-log">org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#1] 

//이하 생략...</code></pre>
<p>참고 자료 출처: <a href="https://www.inflearn.com/course/ORM-JPA-Basic">출처</a></p>
]]></description>
        </item>
    </channel>
</rss>