<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>일단 해보는</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 18 May 2026 01:58:12 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>일단 해보는</title>
            <url>https://images.velog.io/images/eunhye_/profile/941d32d3-ab08-4de3-82af-aa72bc6c43cb/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 일단 해보는. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/eunhye_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Total TypeScript를 만든 Matt Pocock이 본인 .claude/skills 폴더]]></title>
            <link>https://velog.io/@eunhye_/Total-TypeScript%EB%A5%BC-%EB%A7%8C%EB%93%A0-Matt-Pocock%EC%9D%B4-%EB%B3%B8%EC%9D%B8-.claudeskills-%ED%8F%B4%EB%8D%94</link>
            <guid>https://velog.io/@eunhye_/Total-TypeScript%EB%A5%BC-%EB%A7%8C%EB%93%A0-Matt-Pocock%EC%9D%B4-%EB%B3%B8%EC%9D%B8-.claudeskills-%ED%8F%B4%EB%8D%94</guid>
            <pubDate>Mon, 18 May 2026 01:58:12 GMT</pubDate>
            <description><![CDATA[<p>맷 포콕(Matt Pocock)의 <code>skills</code> 저장소는 AI 에이전트(예: Claude Code 등)가 실제 소프트웨어 엔지니어링 작업을 더 효율적으로 수행할 수 있도록 돕는 <strong>&#39;스킬(명령어 및 가이드라인)&#39;</strong>들의 모음집입니다.</p>
<p>각 디렉토리별 주요 구성 내용은 다음과 같습니다.</p>
<h3 id="1-핵심-디렉토리-skills">1. 핵심 디렉토리: <code>skills/</code></h3>
<p>이 저장소의 가장 중요한 부분으로, 용도에 따라 여러 하위 카테고리로 나뉩니다.</p>
<ul>
<li><strong><code>engineering/</code> (엔지니어링 기술)</strong>: 실제 코딩 작업에 사용되는 핵심 도구들입니다.<ul>
<li><strong><code>diagnose</code></strong>: 버그 진단 루프 (재현 -&gt; 가설 -&gt; 수정 -&gt; 테스트).</li>
<li><strong><code>tdd</code></strong>: 테스트 주도 개발(Red-Green-Refactor)을 위한 가이드.</li>
<li><strong><code>grill-with-docs</code></strong>: 계획을 문서(<code>CONTEXT.md</code>, ADR)와 대조하며 검토하는 세션.</li>
<li><strong><code>to-prd</code> / <code>to-issues</code></strong>: 대화 내용을 PRD(제품 요구 사양서)로 만들거나 GitHub 이슈로 분할.</li>
<li><strong><code>improve-codebase-architecture</code></strong>: 코드베이스의 구조적 개선 제안.</li>
<li><strong><code>setup-matt-pocock-skills</code></strong>: 이 스킬들을 사용하기 위한 초기 설정 도구.</li>
</ul>
</li>
<li><strong><code>productivity/</code> (생산성 도구)</strong>: 코딩 외의 일반적인 업무 효율을 높여줍니다.<ul>
<li><strong><code>grill-me</code></strong>: 계획이나 디자인에 대해 AI가 사용자에게 집요하게 질문하여 빈틈을 찾는 기능.</li>
<li><strong><code>caveman</code></strong>: 불필요한 수식어를 빼고 핵심만 전달하여 토큰 사용량을 줄이는 통신 모드.</li>
<li><strong><code>handoff</code></strong>: 현재 작업을 다른 에이전트가 이어받을 수 있게 요약 문서 생성.</li>
<li><strong><code>write-a-skill</code></strong>: 새로운 스킬을 만드는 가이드.</li>
</ul>
</li>
<li><strong><code>personal/</code> (개인용)</strong>: 개인적인 워크플로우를 위한 도구입니다.<ul>
<li><strong><code>obsidian-vault</code></strong>: 옵시디언(Obsidian) 메모 관리 관련.</li>
<li><strong><code>edit-article</code></strong>: 기사나 글 편집 지원.</li>
</ul>
</li>
<li><strong><code>misc/</code> (기타)</strong>: 특정 목적을 위한 도구들입니다.<ul>
<li><strong><code>git-guardrails-claude-code</code></strong>: 위험한 git 명령어(push, reset 등) 실행을 방지하는 가드레일.</li>
<li><strong><code>setup-pre-commit</code></strong>: Husky나 Prettier 같은 프리커밋 훅 설정.</li>
</ul>
</li>
<li><strong><code>in-progress/</code> &amp; <code>deprecated/</code></strong>: 현재 개발 중이거나 더 이상 사용되지 않는 예전 스킬들이 포함되어 있습니다.</li>
</ul>
<h3 id="2-기타-주요-디렉토리-및-파일">2. 기타 주요 디렉토리 및 파일</h3>
<ul>
<li><strong><code>docs/</code></strong>: 프로젝트의 의사결정 기록(ADR - Architecture Decision Records)이 담겨 있습니다. 왜 특정 구조를 선택했는지에 대한 기술적 배경을 알 수 있습니다.</li>
<li><strong><code>scripts/</code></strong>: 스킬들을 시스템에 연결하거나 목록을 확인하는 데 사용되는 쉘 스크립트(<code>link-skills.sh</code>, <code>list-skills.sh</code>)가 들어 있습니다.</li>
<li><strong><code>README.md</code></strong>: 전체 프로젝트의 철학(왜 이 스킬들이 필요한지)과 빠른 시작 가이드가 포함되어 있습니다.</li>
<li><strong><code>CLAUDE.md</code> / <code>CONTEXT.md</code></strong>: AI 에이전트가 이 프로젝트 자체를 이해하고 작업할 때 참고하는 가이드라인 파일입니다.</li>
</ul>
<h3 id="요약-표">요약 표</h3>
<table>
<thead>
<tr>
<th align="left">디렉토리</th>
<th align="left">주요 내용</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong><code>skills/engineering</code></strong></td>
<td align="left">TDD, 버그 진단, 아키텍처 개선 등 실제 개발 업무 지원</td>
</tr>
<tr>
<td align="left"><strong><code>skills/productivity</code></strong></td>
<td align="left">질문 세션(Grilling), 요약 전달(Handoff), 토큰 절약 모드 등</td>
</tr>
<tr>
<td align="left"><strong><code>skills/personal</code></strong></td>
<td align="left">개인 메모(Obsidian) 및 글쓰기 지원</td>
</tr>
<tr>
<td align="left"><strong><code>skills/misc</code></strong></td>
<td align="left">Git 보안 설정, 테스트 마이그레이션 등 기타 유틸리티</td>
</tr>
<tr>
<td align="left"><strong><code>docs/adr</code></strong></td>
<td align="left">프로젝트의 주요 기술적 의사결정 기록</td>
</tr>
<tr>
<td align="left"><strong><code>scripts</code></strong></td>
<td align="left">스킬 설치 및 관리를 위한 실행 스크립트</td>
</tr>
</tbody></table>
<p>이 저장소의 스킬들은 단순히 코드를 짜는 것을 넘어, <strong>&quot;엔지니어링 원칙(Fundamentals)&quot;</strong>을 AI 작업 과정에 녹여내어 더 견고한 소프트웨어를 만드는 데 초점이 맞춰져 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@Mapper(componentModel = "spring")에 의한 에러]]></title>
            <link>https://velog.io/@eunhye_/MappercomponentModel-spring%EC%97%90-%EC%9D%98%ED%95%9C-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@eunhye_/MappercomponentModel-spring%EC%97%90-%EC%9D%98%ED%95%9C-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Thu, 04 Sep 2025 08:19:28 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-현상">1. 문제 현상</h2>
<p>MSA 구조인 프로젝트에서 별도의 branch로 개발 중인 서비스를 실행하려다 공유하고 있는 모듈 중 다른 branch에 있고, 개발중인 서비스에서는 사용도 하지 않고, 참조도 하지 않는 파일에 대한 FileNotFoundException으로 애플리케이션 구동이 실패하였다.</p>
<pre><code>Caused by: java.io.FileNotFoundException: SomethingMapper.class</code></pre><h2 id="2-원인-분석">2. 원인 분석</h2>
<p>해당 파일에는 인터페이스에 다음과 같은 선언이 있었다.</p>
<pre><code>@Mapper(componentModel = &quot;spring&quot;)
public interface SomethingMapper { ... }</code></pre><p>componentModel = &quot;spring&quot; 때문에 MapStruct가 생성한 구현체를 스프링 Bean으로 등록하려고 시도, 
→ 하지만 현재 브랜치/서비스에는 SomethingMapper.class가 존재하지 않음.
→ 스프링이 빈 스캐닝 과정에서 해당 클래스를 찾지 못해 FileNotFoundException 발생.</p>
<p>즉, <strong>공용 라이브러리에 선언된 MapStruct Mapper가 모든 서비스에서 스프링 Bean으로 등록되려 한 것이 원인으로 추정된다.</strong></p>
<h2 id="3-해결-방법">3. 해결 방법</h2>
<p>1) ./gradlew clean build 와 Invalidated Caches를 진행
-&gt;  해결 안됨</p>
<p>2) 원인 파일의 빈 주입 방식 변경 -&gt; 해결</p>
<p>Before
componentModel = &quot;spring&quot; 사용 → 스프링 빈 자동 등록
서비스 코드에서 @Autowired / private final 방식으로 의존성 주입</p>
<p>After
componentModel = &quot;spring&quot; 제거 → 스프링 Bean 등록 안 됨
MapStruct의 정적 팩토리 메서드 방식 사용:</p>
<pre><code>@Mapper
public interface SomethingMapper {
    SomethingMapper INSTANCE = 
        Mappers.getMapper(SomethingMapper.class);
}</code></pre><p>서비스 코드에서 직접 호출:</p>
<pre><code>@Service
@RequiredArgsConstructor
public class CallingService {
...
private final SomethingMapper mapper = 
    SomethingMapper.INSTANCE;
}</code></pre><p>해당 파일을 수정하여 배포 후 에러가 나던 서비스의 Invalidated Caches를 진행하고 원래의 branch에서 Application Run을 해보고, branch를 바꿔  Application Run을 하니 제대로 실행되었다.</p>
<h4 id="정리">정리</h4>
<blockquote>
</blockquote>
<p>@Mapper(componentModel = &quot;spring&quot;) → MapStruct 구현체가 자동으로 Spring Bean이 됨.
이 경우, classpath에 존재하는 모든 Mapper 구현체가 스프링의 스캔 대상이 되며, 멀티 브랜치/멀티 모듈 환경에서는 공용 라이브러리 JAR까지 스캔돼서 예상치 못한 Bean 로딩 문제가 생길 수 있음.
반대로 Mappers.getMapper() 방식을 쓰면 Spring Bean 관리 대상이 아니므로, 다른 브랜치에 있는 클래스까지 영향을 주지 않음.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[InteliJ에 2FA 적용]]></title>
            <link>https://velog.io/@eunhye_/InteliJ%EC%97%90-2FA-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@eunhye_/InteliJ%EC%97%90-2FA-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Mon, 04 Aug 2025 15:43:52 GMT</pubDate>
            <description><![CDATA[<p>회사 GitLab에 2FA가 적용되어 해본 InteliJ에 Git Access Token 적용
별거 아니지만 저 패스워드에 계속 내 패스워드 입력하는 바보 짓을 하고 잇던게 웃겨서 기록,,, </p>
<h3 id="✅-1-gitlab-personal-access-token-생성하기">✅ 1. GitLab Personal Access Token 생성하기</h3>
<ul>
<li><p>GitLab 웹에 로그인</p>
</li>
<li><p>우측 상단 프로필 &gt; &quot;Edit Profile&quot; 클릭</p>
</li>
<li><p>왼쪽 메뉴에서 &quot;Access Tokens&quot; (또는 &quot;Personal Access Tokens&quot;) 선택</p>
</li>
<li><p>아래 항목 설정:</p>
<p>  Name: 예: intellij-token</p>
<p>  Expires at: 원한다면 만료일 설정</p>
<p>  Scopes:</p>
<p>  read_repository (읽기만 할 경우)</p>
<p>  write_repository (푸시도 할 경우)</p>
<p>  api (API 연동이 필요할 경우)</p>
</li>
<li><p>Create personal access token 클릭</p>
<p>생성된 토큰을 복사해두세요. (다시 볼 수 없습니다!)</p>
</li>
</ul>
<h3 id="✅-2-intellij에서-gitlab-인증-설정">✅ 2. IntelliJ에서 GitLab 인증 설정</h3>
<p>방법 A: Git 설정에서 토큰 사용</p>
<ul>
<li>IntelliJ에서 Preferences 또는 Settings 열기 (Cmd+, 또는 Ctrl+Alt+S)</li>
<li>Version Control &gt; Git 메뉴로 이동</li>
<li>Path to Git executable 아래의 Test 버튼으로 git 설치 확인</li>
<li>&quot;Use credential helper&quot; 또는 &quot;Native&quot; 선택</li>
</ul>
<h3 id="✅-3-git-작업-시-주의-사항">✅ 3. Git 작업 시 주의 사항</h3>
<p>git push 또는 git pull 시 인증을 요구하면:</p>
<pre><code>Username: GitLab 사용자명

Password: 생성한 Personal Access Token 사용</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[외부 API 호출 재시도 ]]></title>
            <link>https://velog.io/@eunhye_/%EC%99%B8%EB%B6%80-API-%ED%98%B8%EC%B6%9C-%EC%9E%AC%EC%8B%9C%EB%8F%84</link>
            <guid>https://velog.io/@eunhye_/%EC%99%B8%EB%B6%80-API-%ED%98%B8%EC%B6%9C-%EC%9E%AC%EC%8B%9C%EB%8F%84</guid>
            <pubDate>Fri, 25 Jul 2025 07:53:32 GMT</pubDate>
            <description><![CDATA[<p>MSA 서비스를 개발하면서 외부 API를 호출하는 일이 많은데, 외부 API 호출에 실패할 경우 재시도를 적용할 때, 사용했던 2가지 방법에 대해 기록.</p>
<h2 id="queue-를-사용한-재시도">Queue 를 사용한 재시도</h2>
<p>큐에 Delay 적용하여 재시도 횟수 별로 시간 차를 두어 호출하게 한다.</p>
<p>정보 유실이 일어나면 안되는 메시지나 빠르게 정보를 취합하는 것 보다 확실한 이벤트 전달이 중요할 때.
Ex) 외부 서비스 웹 훅 이벤트 연동 등,,</p>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/6c2c73f6-f8ec-4a04-94b1-3fb11bfbd29f/image.png" alt=""></p>
<ul>
<li><p>장점 
큐에 저장되기 때문에 서버 재시작 등으로 정보가 유실되지 않는다.⭐
메인 서버 트랜잭션과 분리되어 있기 때문에 서버 부하를 최소화 한다.
큐 시스템 자체 설정에 따라 대용량 트래픽에 대응이 가능하다.</p>
</li>
<li><p>단점
기능 구현에 복잡성이 있다.
큐 시스템 운영 및 모니터링이 필요하다. </p>
</li>
</ul>
<h2 id="resilience4j의-retry를-활용한-재시도">Resilience4j의 Retry를 활용한 재시도</h2>
<p>RetryConfig 에 Duration 규칙을 두어 재시도 횟수별로 시간 차를 두었다. </p>
<p>Resilience4j의 Retry를 사용할 경우 시간 차를 길게 둘 수 없고 너무 많은 호출에 적용하면 서버 부하가 예상된다. 
또한 데이터를 생성하거나 수정하는 호출에 경우 멱등성이 회손 될 가능성이 있다.
그렇기 때문에 호출 빈도수가 높고, 서비스 노출성이 높은 외부 API 조회성 호출에 적용하였다. </p>
<p>이 경우 서버 재시작으로 정보가 유실 되어도 Client 단에서 재접속시 정보를 획득 할 수 있고, 단기간 내에 정보 취득이 필요하다고 판단하여 이 방법을 선택하였다.</p>
<p>EX) 서비스에 필요한 정보를 외부 API를 통해 가져오는 건</p>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/8d565628-28ff-4dac-a4c7-d9112941ecab/image.png" alt=""></p>
<ul>
<li><p>장점
구현이 간단하다.
JVM 내에서 처리 가능하다.</p>
</li>
<li><p>단점
재시도 대기 중인 요청은 Thread와 자원을 점유하기 때문에 서버 메모리에 부담이 된다.
서버 재시작 등의 이슈가 있을 시 재시도 정보가 유실 될 수 있다.
텀이 긴 재시도는 불가능 하다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SAGA 패턴의 동작 방식]]></title>
            <link>https://velog.io/@eunhye_/SAGA-%ED%8C%A8%ED%84%B4%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@eunhye_/SAGA-%ED%8C%A8%ED%84%B4%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Thu, 27 Mar 2025 07:00:16 GMT</pubDate>
            <description><![CDATA[<p>SAGA는 두 가지 전략으로 동작합니다.</p>
<h3 id="1-choreography-코레오그래피-방식">1. Choreography (코레오그래피) 방식</h3>
<ul>
<li>이벤트 기반으로 서비스들이 서로 통신하면서 트랜잭션을 관리하는 방식</li>
<li>중앙 컨트롤러 없이 각 서비스가 이벤트를 보고 반응</li>
</ul>
<p>예시 주문 트랜잭션</p>
<p>Order Service에서 주문 생성 이벤트 발행 (OrderCreated)</p>
<p>Payment Service가 이를 받아 결제 승인 (PaymentApproved)</p>
<p>Inventory Service가 재고 차감 (InventoryUpdated)</p>
<p>실패하면 <strong>보상 이벤트(OrderCancelled)</strong>를 발행하여 롤백</p>
<p>✅ 장점</p>
<p>서비스 간 느슨한 결합 (각 서비스가 독립적으로 동작)</p>
<p>확장성(새로운 서비스 추가가 쉬움)</p>
<p>❌ 단점</p>
<p>서비스 간 이벤트 흐름이 복잡해질 수 있음</p>
<p>디버깅과 트랜잭션 추적이 어려움</p>
<h3 id="2-orchestration-오케스트레이션-방식">2. Orchestration (오케스트레이션) 방식</h3>
<ul>
<li>중앙에서 하나의 <strong>SAGA Coordinator(조정자)</strong>가 전체 트랜잭션을 관리</li>
<li>중앙 컨트롤러가 서비스 호출을 순차적으로 수행하고, 실패 시 롤백을 실행</li>
</ul>
<p>예시 주문 트랜잭션</p>
<p>SAGA Coordinator가 Order Service에 주문 생성 요청</p>
<p>Order Service가 성공하면 Payment Service에 결제 요청</p>
<p>Payment Service가 성공하면 Inventory Service에 재고 차감 요청</p>
<p>어느 한 단계라도 실패하면, 이전 서비스들에 보상 트랜잭션 수행</p>
<p>예: 결제 성공 후 재고 부족 → Payment Service에 결제 취소 요청</p>
<p>✅ 장점</p>
<p>트랜잭션 흐름이 명확하고 관리가 쉬움</p>
<p>디버깅 및 모니터링이 용이</p>
<p>❌ 단점</p>
<p>중앙 SAGA Coordinator에 의존 → 단일 장애점(Single Point of Failure, SPOF) 위험</p>
<p>서비스 간 결합도가 증가할 가능성이 있음</p>
<h2 id="단일-서비스에서-saga-패턴을-유사하게-적용하는-방법">단일 서비스에서 SAGA 패턴을 유사하게 적용하는 방법</h2>
<p>(1) @Transactional을 활용한 트랜잭션 처리 (Repository 간 트랜잭션)
JPA 기반 Repository는 <strong>@Transactional</strong>을 활용하여 자동 롤백이 가능합니다.</p>
<pre><code class="language-java">@Service
public class OrderService {
    @Autowired private OrderRepository orderRepository;
    @Autowired private PaymentRepository paymentRepository;

    @Transactional
    public void processOrder(Order order) {
        orderRepository.save(order); // 주문 저장
        paymentRepository.save(order.getPayment()); // 결제 저장
        // 만약 여기서 예외 발생 시, 두 작업 모두 롤백됨
    }
}</code></pre>
<ul>
<li>이 방식은 같은 DB 내의 여러 Repository를 조작하는 경우 적합</li>
<li>예외 발생 시 전체 트랜잭션이 롤백됨
❌ 외부 API 호출이 포함될 경우에는 적용되지 않음</li>
</ul>
<p>(2) 외부 API와 함께 사용할 때는 보상 트랜잭션 적용</p>
<ul>
<li>@Transactional은 DB 내 트랜잭션만 관리 가능하므로 외부 API 호출이 포함되면 별도의 보상 트랜잭션이 필요함.</li>
</ul>
<p>ex) 주문 저장 + 결제 API 호출</p>
<pre><code>@Service
public class OrderService {
    @Autowired private OrderRepository orderRepository;
    @Autowired private PaymentService paymentService;

    public void processOrder(Order order) {
        try {
            orderRepository.save(order); // 1. 주문 저장
            paymentService.processPayment(order); // 2. 외부 결제 API 호출
        } catch (Exception e) {
            compensateOrder(order); // 실패 시 보상 트랜잭션 실행
        }
    }

    private void compensateOrder(Order order) {
        orderRepository.delete(order); // 주문 취소 (보상 트랜잭션)
    }
}</code></pre><ul>
<li>주문이 저장된 후 결제 API 호출 중 실패하면 주문을 취소하는 &quot;보상 트랜잭션&quot; 실행
❌ 하지만, 이 방식은 여러 서비스 간의 비동기 이벤트 흐름을 관리하는 것이 아님</li>
</ul>
<p>(3) 이벤트 기반 SAGA 방식 적용 (Kafka, RabbitMQ 활용)
만약 하나의 서비스 내에서 여러 개의 API와 DB 트랜잭션을 관리해야 한다면, 이벤트 기반으로 관리 가능</p>
<p>Kafka 같은 메시지 큐를 사용하여 비동기 방식으로 트랜잭션을 보상 처리</p>
<p>ex)</p>
<pre><code>@Service
public class OrderService {
    @Autowired private KafkaTemplate&lt;String, String&gt; kafkaTemplate;
    @Autowired private OrderRepository orderRepository;

    public void processOrder(Order order) {
        orderRepository.save(order);
        kafkaTemplate.send(&quot;order_created&quot;, order.getId().toString()); // 이벤트 발행
    }
}

@Component
@KafkaListener(topics = &quot;order_created&quot;)
public class PaymentService {
    @Autowired private PaymentRepository paymentRepository;

    public void processPayment(String orderId) {
        try {
            paymentRepository.save(new Payment(orderId));
        } catch (Exception e) {
            kafkaTemplate.send(&quot;order_failed&quot;, orderId); // 실패 시 보상 이벤트 발행
        }
    }
}

@Component
@KafkaListener(topics = &quot;order_failed&quot;)
public class CompensationService {
    @Autowired private OrderRepository orderRepository;

    public void compensateOrder(String orderId) {
        orderRepository.deleteById(orderId); // 보상 트랜잭션 실행
    }
}</code></pre><ul>
<li>Kafka를 활용하여 SAGA 패턴처럼 보상 트랜잭션을 수행</li>
<li>비동기 이벤트 기반으로 처리 가능
❌ 구현이 복잡하고 메시지 큐를 관리해야 함</li>
</ul>
<ol start="3">
<li>언제 어떤 방법을 쓰는게 좋을지</li>
</ol>
<p>@Transactional    DB의 여러 Repository를 한 트랜잭션으로 처리할 때</p>
<ul>
<li>간단하고 안정적    외부 API 포함 시 트랜잭션 관리 불가<ul>
<li>보상 트랜잭션 (Compensating Transaction)    외부 API가 포함된 트랜잭션 처리 시    예외 발생 시 롤백 가능    수동으로 롤백 로직을 구현해야 함</li>
</ul>
</li>
</ul>
<p>이벤트 기반 처리 (Kafka, RabbitMQ)    API &amp; DB 작업이 많고 비동기 처리가 필요한 경우    </p>
<ul>
<li>비동기 트랜잭션 관리 가능, MSA에서 확장 가능    구현이 복잡하고 메시지 큐 필요</li>
</ul>
<p>💡결론
단일 서비스 내에서 여러 개의 Repository를 다룰 때는 @Transactional을 사용하자.</p>
<p>외부 API가 포함될 경우 보상 트랜잭션(Compensating Transaction)을 구현하자.</p>
<p>비동기 이벤트 기반 처리가 필요하면 Kafka를 활용하여 SAGA 패턴과 유사한 흐름을 만들자.</p>
<h4 id="즉-단일-서비스에서는-전통적인-트랜잭션-관리-방식transactional--보상-트랜잭션을-활용하고-필요에-따라-이벤트-기반-방식도-고려하는-것이-좋다">즉, 단일 서비스에서는 전통적인 트랜잭션 관리 방식(@Transactional + 보상 트랜잭션)을 활용하고, 필요에 따라 이벤트 기반 방식도 고려하는 것이 좋다!</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[Generate Short URL Service]]></title>
            <link>https://velog.io/@eunhye_/Generate-Short-URL-Service</link>
            <guid>https://velog.io/@eunhye_/Generate-Short-URL-Service</guid>
            <pubDate>Thu, 26 Sep 2024 08:20:01 GMT</pubDate>
            <description><![CDATA[<p>ShortURL 생성 서비스</p>
<h2 id="aws-아키텍쳐-구성">AWS 아키텍쳐 구성</h2>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/338cb615-a8f3-4451-825f-5119f876b5b3/image.png" alt="">
<strong>API Gateway</strong></p>
<p>Client에서 server로 통신할 때 사용하는 많은 api들의 대문(게이트웨이).</p>
<p>라우팅 설정에 따라 각 엔드포인트로 클라이언트를 대리하여 요청하고 응답을 받으면 다시 클라이언트에게 전달하는 프록시(proxy) 역할을 한다.</p>
<p><strong>Lambda</strong></p>
<p>특정한 경우에만 실행시키는 Serverless 컴퓨팅 플랫폼,</p>
<p><strong>DynamoDB</strong></p>
<p>AWS에서 제공하는 no-sql 데이터베이스.</p>
<h2 id="시퀀스-다이어그램">시퀀스 다이어그램</h2>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/b2ff4345-7a37-4fbb-96a5-f66aad662644/image.png" alt=""></p>
<p><strong>Base62 인코딩 방식</strong></p>
<p>URL에서는 /, + 기호가 구분자로 사용되기 때문에, 인코딩을 했을 때에 이 문자들이 포함된다면 기대값과 다르게 동작할 수 있다. Base62는 위 2가지 케이스를 제외하고 URL-safe 한 문자들만 모든 방식이라고 할 수 있겠다.</p>
<p>Referrence
<a href="https://caputdraconis.tistory.com/entry/AWS-%EA%B0%9C%EC%9D%B8-URL-Shortener-%EB%A7%8C%EB%93%A4%EA%B8%B0">https://caputdraconis.tistory.com/entry/AWS-%EA%B0%9C%EC%9D%B8-URL-Shortener-%EB%A7%8C%EB%93%A4%EA%B8%B0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Error]Rest Docs 빌드 시 바로 반영 안되는 이슈 ]]></title>
            <link>https://velog.io/@eunhye_/ErrorRest-Docs-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@eunhye_/ErrorRest-Docs-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Mon, 01 Jan 2024 13:48:04 GMT</pubDate>
            <description><![CDATA[<p>ci-cd 과정에서 빌드 시 변경된 코드가 REST docs에 바로 반영되지 않았던 이슈 </p>
<p>before</p>
<pre><code>bootJar {
    dependsOn asciidoctor
    from(&quot;${asciidoctor.outputDir}&quot;) {
        into &#39;static/api-docs&#39;
    } 
}
</code></pre><p>after</p>
<pre><code>bootJar {
    from(&quot;${asciidoctor.outputDir}&quot;) {
        into &#39;static/api-docs&#39;
    }
    dependsOn asciidoctor
}

</code></pre><p>build.gradle에서 bootJar 부분에 dependsOn asciidoctor가 위에 있으면
asciidoctor.outputDir가 활성화 되지 않았다. </p>
<p>다른 부분은 해당 이슈에 대해서 아래 참조 블로그나 검색을 통해 참고할 수 있는 부분이 
많이 있었으나 위 부분은 사소한 부분이라 그런지 언급이 없어서 적어 놓기로 하였다.</p>
<p>아래 참조 블로그 글을 통해 REST docs와 이슈에 대해 좀 더 확실히 이해할 수 있게 되었다. 
<a href="https://velog.io/@ohzzi/REST-Docs-%EC%96%B4%EB%94%9C-%EB%B3%B4%EC%8B%9C%EB%8A%94-%EA%B1%B0%EC%A3%A0-%EA%B7%B8%EA%B1%B4-%EC%A0%9C-%EC%9E%94%EC%83%81%EC%9E%85%EB%8B%88%EB%8B%A4%EB%A7%8C">참조: 그건 제 잔상입니다만..</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React + Nginx 를 도커 이미지로 만들어서 배포하기(React Project Docker Image Resize)]]></title>
            <link>https://velog.io/@eunhye_/React-Nginx-%EB%A5%BC-%EB%8F%84%EC%BB%A4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0React-Project-Docker-Image-Resize</link>
            <guid>https://velog.io/@eunhye_/React-Nginx-%EB%A5%BC-%EB%8F%84%EC%BB%A4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0React-Project-Docker-Image-Resize</guid>
            <pubDate>Tue, 22 Aug 2023 07:06:24 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@tlatjdgh3778/React-Nginx-%EB%A5%BC-%EB%8F%84%EC%BB%A4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0">React + Nginx 를 도커 이미지로 만들어서 배포하기</a></p>
<pre><code># base image 설정(as build 로 완료된 파일을 밑에서 사용할 수 있다.)
FROM node:16.19.0-alpine as builder

# root 에 app 폴더를 생성
RUN mkdir /app

# work dir 고정
WORKDIR /app

COPY . .

RUN apk --no-cache --virtual build-dependencies add \
    python3 \
    make \
    g++ \
    &amp;&amp; apk del build-dependencies

RUN apk --update add rsync

ENV PATH /usr/src/app/node_modules/.bin:$PATH

# RUN npm install 
# 안될 시
RUN npm install --legacy-peer-deps 

# 호스트 머신의 현재 디렉토리 파일들을 컨테이너 내부로 전부 복사
COPY . .

# npm build
RUN npm run build

# nginx 이미지를 사용합니다. 뒤에 tag가 없으면 latest 를 사용합니다.
FROM nginx:1.25-alpine

# 이전 빌드 단계에서 빌드한 결과물을 /app/build 으로 복사한다.
COPY --from=builder /app/build /app/build

# nginx 의 default.conf 를 삭제
RUN rm /etc/nginx/conf.d/default.conf

# host pc 의 nginx.conf 를 아래 경로에 복사
COPY ./nginx.conf /etc/nginx/conf.d

# 80 포트 오픈
EXPOSE 80

# container 실행 시 자동으로 실행할 command. nginx 시작함
CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[nexus에 docker image 등록]]></title>
            <link>https://velog.io/@eunhye_/nexus%EC%97%90-docker-image-%EB%93%B1%EB%A1%9D</link>
            <guid>https://velog.io/@eunhye_/nexus%EC%97%90-docker-image-%EB%93%B1%EB%A1%9D</guid>
            <pubDate>Tue, 22 Aug 2023 06:54:03 GMT</pubDate>
            <description><![CDATA[<p>docker login nexus-url</p>
<p>docker pull image</p>
<p>#docker image list 확인
docker image list</p>
<p>docker tag nexus-url:port/path/image:tag
#tag 전체경로
<img src="https://velog.velcdn.com/images/eunhye_/post/ff7e63f9-2bb0-49a9-93cd-5d004950d2e9/image.png" alt=""></p>
<p>docker push nexus-url:port/path/image:tag
<img src="https://velog.velcdn.com/images/eunhye_/post/7294e7cf-29bf-4fff-8fb7-d8a6728e9f8f/image.png" alt=""></p>
<p>nexus image 확인
<img src="https://velog.velcdn.com/images/eunhye_/post/adb8e56c-f6e1-4506-b795-02d5327cf048/image.png" alt=""></p>
<p>#docker image 삭제
docker rmi imageId
<img src="https://velog.velcdn.com/images/eunhye_/post/8dbdd567-136c-48dd-9a7b-8cf93f5071aa/image.png" alt=""></p>
<p>#삭제 후 이미지 리스트 확인
docker image list</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[docker ce]No space left on device?]]></title>
            <link>https://velog.io/@eunhye_/docker-ceNo-space-left-on-device</link>
            <guid>https://velog.io/@eunhye_/docker-ceNo-space-left-on-device</guid>
            <pubDate>Fri, 09 Jun 2023 08:54:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/eunhye_/post/4b78cd72-e8a3-4843-9fb0-4c141858218b/image.png" alt=""></p>
<p>No space left on device: 장치에 남은 공간이 없음
배포 후 발생하는 에러를 확인해보니 아래와 같은 메시지가 확인 되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/10aac99f-c3fa-4603-935c-651ce1b3f6d1/image.png" alt="">
우선은 서버 용량을 추가하고, docker에서 불필요하게 차지하는 부분을 없앨 방법을 찾아 보았습니다.</p>
<h3 id="docker-system-df--v">docker system df -v</h3>
<p>💡 docker system df : 현재 사용중인 이미지, 컨테이너 및 볼륨이 얼마나 많은 공간을 사용하고 있는지 확인할 수 있는 명령어. 뒤에 -v (vervose)옵션을 추가하면 사용하지 않는 이미지와 컨테이너도 확인 가능
<img src="https://velog.velcdn.com/images/eunhye_/post/12039de5-25a4-4c5a-945e-8a18ca28aa33/image.png" alt=""></p>
<h3 id="docker-prune">docker prune</h3>
<p>💡 docker prune : 사용하지 않는 컨테이너 이미지를 제거하는 명령어. prune의 가장 큰 특징은 어떠한 옵션을 주는가에 따라 그 역할이 달라진다는 것</p>
<p>option
docker volume prune : 미사용 볼륨 제거
docker container prune : 미사용 컨테이너 제거
docker image prune : 미사용 이미지 제거
docker system prune : 미사용 중인 이미지, 컨테이너, 볼륨 모두 제거</p>
<p>우선적으로 docker systme prune 명령어를 통해 일부 용량을 확보 할 수 있었습니다.</p>
<p>추가적으로 재발 방지를 위해, 아래와 같이 일주일이 지난 컨테이너 및 이미지 등에 대하여 주기적으로 삭제하는 크론탭을 daily.cron에 등록하였습니다. prune 명령어에는 —-filter 옵션이 있는데 이를 잘 활용하면 운영에 큰 이점이 있습니다.</p>
<pre><code>docker system prune -af --filter &quot;until=$((7*24))h&quot;</code></pre><p>참고 : <a href="https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-No-space-left-on-device-Docker-prune">https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-No-space-left-on-device-Docker-prune</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[docker ce] 도커 이미지 업로드 권한 에러 (denied: requested access to the resource is denied)]]></title>
            <link>https://velog.io/@eunhye_/docker-ce-%EB%8F%84%EC%BB%A4-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B6%8C%ED%95%9C-%EC%97%90%EB%9F%AC-denied-requested-access-to-the-resource-is-denied</link>
            <guid>https://velog.io/@eunhye_/docker-ce-%EB%8F%84%EC%BB%A4-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B6%8C%ED%95%9C-%EC%97%90%EB%9F%AC-denied-requested-access-to-the-resource-is-denied</guid>
            <pubDate>Fri, 09 Jun 2023 08:48:20 GMT</pubDate>
            <description><![CDATA[<p>이미지를 docker hub 에 업로드할 때 &quot;denied: requested access to the resource is denied&quot; 에러가 발생할 수 있습니다.</p>
<ol>
<li>docker hub에 로그인이 되지 않을 경우</li>
<li>user name과 docker hub 에 로그인된 ID가 일치하지 않을 경우</li>
</ol>
<h2 id="1-로그인하지-않아서-발생한-경우">1. 로그인하지 않아서 발생한 경우</h2>
<p> docker hub 회원가입 및 docker login 입력하여 로그인 진행
 <strong>* shell에서 docker login 시 계정이름 사용할 것! not email!</strong></p>
<h2 id="2-이미지의-user-name과-docker-hub에-가입된-id가-일치하지-않을-경우">2. 이미지의 user name과 docker hub에 가입된 ID가 일치하지 않을 경우</h2>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/632530c1-ac75-4db8-a6c8-05e5f0c98ebf/image.png" alt="">
아래 이미지에서 노란색으로 표시한 부분이 이미지의 user name 입니다. 기존에 만들어둔것은 nirsa 인데, docker hub (<a href="https://hub.docker.com/">https://hub.docker.com/</a>) 에 가입했던 ID와 달랐습니다.</p>
<pre><code>//기존 이미지 삭제
$ docker image rm [image-name]

// 새 이미지에 user name과 통일시켜 생성
... .gitlab-ci.yml
- docker build -t $DOCKER_HUB_ID/$IMAGE_NAME:latest .
...
</code></pre><p>referrence : <a href="https://nirsa.tistory.com/53">https://nirsa.tistory.com/53</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ubuntu]ubuntu setting(docker, gitlab-runner)]]></title>
            <link>https://velog.io/@eunhye_/ubuntuubuntu-settingdocker-gitlab-runner</link>
            <guid>https://velog.io/@eunhye_/ubuntuubuntu-settingdocker-gitlab-runner</guid>
            <pubDate>Fri, 09 Jun 2023 08:29:00 GMT</pubDate>
            <description><![CDATA[<h3 id="docker-설치">docker 설치</h3>
<p>Ubuntu에 Docker를 설치하는 것은 매우 간단합니다. Docker 저장소를 활성화하고 저장소 GPG 키를 가져온 후 패키지를 설치합니다.</p>
<pre><code>$ sudo apt update

// 새 HTTPS 리포지토리를 추가하는 데 필요한 종속성을 설치
$ sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common

// 리포지토리의 GPG 키를 가져옴
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

// Docker APT 리포지토리를 시스템에 추가
$ sudo add-apt-repository &quot;deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&quot;

// 최신 버전의 도커를 설치
$ sudo apt update
$ sudo apt install docker-ce docker-ce-cli containerd.io

// 특정 Docker 버전을 설치
$ apt list -a docker-ce
$ sudo apt install docker-ce=&lt;VERSION&gt; docker-ce-cli=&lt;VERSION&gt; containerd.io

// 설치가 완료되면 도커 서비스가 자동으로 시작
$ sudo systemctl status docker
# ● docker.service - Docker Application Container Engine
#      Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
#      Active: active (running) since Thu 2020-05-21 14:47:34 UTC; 42s ago
# ...

//도커 패키지가 업데이트되지 않도록 하려면 보류로 표시
$ sudo apt-mark hold docker-ce

// root외 유저에 도커 권한 부여
$ sudo usermod -aG docker $USER

// 설치 확인
$ docker container run hello-world
or
$ sudo docker version</code></pre><p>docker 설치 참고
<a href="https://jjeongil.tistory.com/1968">https://jjeongil.tistory.com/1968</a>
<a href="https://dongle94.github.io/docker/docker-ubuntu-install/">https://dongle94.github.io/docker/docker-ubuntu-install/</a></p>
<h3 id="gitlab-runner-설치">gitlab-runner 설치</h3>
<p>gitlab-runner를 docker와 사용하려면 docker 설치 필요!</p>
<pre><code>$ su - root

$ curl -L &quot;https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh&quot; | sudo bash

$ sudo apt install gitlab-runner

$ sudo gitlab-runner register

Q: Enter the GitLab instance URL (for example, https://gitlab.com/):  (GitLab URL 입력하기)
I: https://gitlab.com/  (위에서 확인한 URL 카피하여 복사)

Q: Enter the registration token:  (GitLab Repository Token 입력하기)
I: ~~~~CA~~~~ (위에서 확인한 token 카피하여 복사)

Q: Enter a description for the runner:  (Runner Description 입력하기)
I: ex) GitLab Runner Test

Q: Enter tags for the runner (comma-separated):  (태그 입력하기, 여기서 입력된 태그는 gitlab-ci.yml파일에서 등록된 runner를 구분하기 위해 사용)
I: ex) dev-all

Q: Enter an executor: docker-ssh+machine, custom, parallels, shell, virtualbox, docker+machine, docker, docker-ssh, ssh, kubernetes:  (빌드에 사용되는 executor 선택)
I: ex) docker

Q: Enter the default Docker image (for example, ruby:2.6):  (Docker image default 입력)
I: docker:20.10.12  (위에서 확인한 docker 버전 입력)
or docker:latest

// runner 연결 확인
$ gitlab-runner list

// Docker를 사용해서 배포하는 경우
$ sudo usermod -aG docker gitlab-runner

// GitLab CI에 작성된 docker 명령어들은 모두 dind로 수행
// runner의 설정에 다음의 값들을 수정 및 추가해줘야 함
$ vi /etc/gitlab-runner/config.toml
... 
[[runners]]
  [runners.docker]
    privileged = true
    volumes = [&quot;/cache&quot;, &quot;/var/run/docker.sock:/var/run/docker.sock&quot;]
    network_mode = &quot;host&quot;
    pull_policy = [&quot;always&quot;]
 ...
 :wq //저장 후 나감

 $ service docker restart // 도커 재시작</code></pre><p>gitlab-runner 설치 참고
<a href="https://yscho03.tistory.com/m/133">https://yscho03.tistory.com/m/133</a>
<a href="https://jforj.tistory.com/227">https://jforj.tistory.com/227</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ubuntu]ubuntu setting(java, gradle, mysql)]]></title>
            <link>https://velog.io/@eunhye_/ubuntuubuntu-settingjava-gradle-mysql</link>
            <guid>https://velog.io/@eunhye_/ubuntuubuntu-settingjava-gradle-mysql</guid>
            <pubDate>Fri, 09 Jun 2023 08:04:47 GMT</pubDate>
            <description><![CDATA[<h3 id="java-17-설치">java 17 설치</h3>
<pre><code> - $ sudo apt update
 - $ sudo apt install openjdk-17-jdk
 - $ java -version 
 #경로  
 - default : /usr/lib/jvm/java-[버전]-openjdk-amd64/bin/java
 - $ sudo vi /etc/environment
 PATH=&quot;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin&quot;
 JAVA_HOME=&quot;/usr/lib/jvm/java-17-openjdk-amd64&quot;
  - $ source /etc/environment
  - $ echo $JAVA_HOME
  /usr/lib/jvm/java-17-openjdk-amd64</code></pre><p><a href="https://languagestory.tistory.com/154">java 설치 참고</a></p>
<h3 id="gradle-설치">gradle 설치</h3>
<pre><code>$ sudo apt update
$ VERSION=8.0.2
$ wget https://services.gradle.org/distributions/gradle-${VERSION}-bin.zip -P /tmp
$ sudo unzip -d /opt/gradle /tmp/gradle-${VERSION}-bin.zip
# gradle 설치 디렉터리를 가리키는 latest라는 이름의 심볼 링크를 만듬.
$ sudo ln -s /opt/gradle/gradle-${VERSION} /opt/gradle/latest

#환경변수 설정
$ sudo nano /etc/profile.d/gradle.sh
export GRADLE_HOME=/opt/gradle/latest
export PATH=${GRADLE_HOME}/bin:${PATH}
:wq
$ sudo chmod +x /etc/profile.d/gradle.sh
$ source /etc/profile.d/gradle.sh

# 설치 확인
$ gradle -v
# Welcome to Gradle 6.5.1!
# 
# Here are the highlights of this release:
#  - Experimental file-system watching
#  - Improved version ordering
#  - New samples
# 
# For more details see https://docs.gradle.org/8.0.2/release-notes.html
# 
# 
# ------------------------------------------------------------
# Gradle 8.0.2
# ------------------------------------------------------------
# 
#...</code></pre><h3 id="mysql-설치">mysql 설치</h3>
<pre><code>$ sudo apt-get update

$ sudo apt-get install mysql-server
# 설치 중간 중간 Password를 물어보는데 빈칸으로 두고 OK를 누르면 Ubuntu 서버 비밀번호와 자동으로 동기화 된다.

# 외부 접속 기능 설정 (포트 3306 오픈)
$ sudo ufw allow mysql

# MySQL 실행
$ sudo systemctl start mysql

# Ubuntu 서버 재시작시 MySQL 자동 재시작 - 옵션
$ sudo systemctl enable mysql

# MySQL 접속
$ sudo /usr/bin/mysql -u root -p</code></pre><p>참고 : <a href="https://velog.io/@seungsang00/Ubuntu-%EC%9A%B0%EB%B6%84%ED%88%AC%EC%97%90-MySQL-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0">ubuntu에 mysql 설치</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] Mac OS에 Docker 설치]]></title>
            <link>https://velog.io/@eunhye_/Docker-Mac-OS%EC%97%90-Docker-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@eunhye_/Docker-Mac-OS%EC%97%90-Docker-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Sun, 02 Apr 2023 10:46:25 GMT</pubDate>
            <description><![CDATA[<h2 id="도커란">도커란?</h2>
<blockquote>
<p> 리눅스의 응용 프로그램들을 프로세스 격리 기술들을 사용해 컨테이너(container)로 실행하고 관리하는 오픈 소스 프로젝트이다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/2baa1023-a2be-4c45-8962-a856333934ae/image.png" alt=""></p>
<p>서버에서 다양한 프로그램, 실행환경을 _컨테이너_로 추상화하고 동일한 인터페이스를 제공하여 프로그램의 배포 및 관리를 단순하게 해준다. 백엔드 프로그램, 데이터베이스 서버, 메시지 큐등 어떤 프로그램도 컨테이너로 추상화할 수 있고 조립PC, AWS, Azure, Google cloud등 어디에서든 실행할 수 있다.</p>
<p>그래서 대부분의 서버 사이드 개발회사들이 &quot;도커&quot;라는 프로그램(컨테이너)을 도입하기 시작했다.</p>
<h4 id="컨테이너">컨테이너?</h4>
<blockquote>
<p>컨테이너는 영어 뜻 그대로 무언가를 담는 &quot;그릇, 용기, 통&quot; 등을 의미한다. 도커는 어느 환경이든 구애받지 않고 어플리케이션 환경을 그대로 푹(?)떠서 여기서 저기로 저기서 여기로 옮기는 컨테이너같은 역할을 한다. 그래서 도커는 곧 &quot;컨테이너&quot;라는 개념으로 통용된다.</p>
</blockquote>
<h2 id="도커-설치">도커 설치</h2>
<h3 id="설치-파일-다운받기">설치 파일 다운받기</h3>
<p>공식 사이트 또는 Docker hub에서 본인 사양에 맞는 설치 파일을 다운로드한다.
<a href="https://docs.docker.com/desktop/install/mac-install/">도커 공식 홈페이지</a></p>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/0fc7277f-33b0-426f-99f9-308ae82c73c0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/bd99b621-64f4-47ce-8c4c-52a780649b3f/image.png" alt="">
-&gt; 다운로드한 .dmg 파일을 실행하여 Docker.app을 응용 프로그램으로 복사해준다.</p>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/d400a0fb-1e6b-4223-8dd7-8ba091816664/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/57cfcd46-2c7d-40cc-bed7-ae8b513b0159/image.png" alt=""></p>
<p>-&gt; Docker Desktop을  실행하면 메뉴 막대 상단에 아이콘이 생성된다. </p>
<h3 id="설치-확인">설치 확인</h3>
<p>터미널에서 다음 명령어를 통해 정상 설치 유무를 확인할 수 있다.</p>
<ul>
<li>docker</li>
<li>docker --version</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/f58f9c27-a583-4a50-b309-38b39047a888/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/97a5cc8a-54e2-475a-a529-0681f4ea642b/image.png" alt=""></p>
<p><em>Refference</em>
<a href="https://goddaehee.tistory.com/312">[ Docker Desktop] Mac용 Docker Desktop 설치하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring]스프링의 다양한 예외 처리 방법..(1/n)]]></title>
            <link>https://velog.io/@eunhye_/spring%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%98-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EB%B0%A9%EB%B2%95..1n</link>
            <guid>https://velog.io/@eunhye_/spring%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%98-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EB%B0%A9%EB%B2%95..1n</guid>
            <pubDate>Sat, 01 Apr 2023 12:52:54 GMT</pubDate>
            <description><![CDATA[<h2 id="execptionhandler">@ExecptionHandler</h2>
<p>@ExceptionHandler는 Controller계층에서 발생하는 에러를 잡아서 메서드로 처리해주는 기능이다.
Service, Repository에서 발생하는 에러는 제외한다.</p>
<h3 id="controller계층에서-발생하는-에러">Controller계층에서 발생하는 에러</h3>
<pre><code class="language-java">@Controller
public class TestController {

    // ...

    @ExceptionHandler
    public ResponseEntity&lt;String&gt; handle(IOException ex) {
        // ...
    }
}</code></pre>
<p>이렇게 @Controller로 선언된 클래스 안에서 @ExceptionHandler 어노테이션으로 메서드 안에서 발생할 수 있는 에러를 처리할 수 있다.</p>
<h3 id="여러개의-exception-처리">여러개의 Exception 처리</h3>
<p>@ExceptionHandler의 value 값으로 어떤 Exception을 처리할 것인지 넘겨줄 수 있는데, value를 설정하지 않으면 모든 Exception을 잡게 되기 때문에 Exception을 구체적으로 적어주는 것이 좋다고 한다.</p>
<p>여러 개의 Exception이 발생할 수 있는 코드가 있다고 하자.</p>
<pre><code class="language-java">@Controller
public class TestController {

    // ...

    @ExceptionHandler({FileSystemException.class, RemoteException.class})
    public ResponseEntity&lt;String&gt; handle(Exception ex) {
        // ...
    }
}</code></pre>
<p>메서드의 인자로 Exception ex를 받고 있고 @ExceptionHandler의 value값으로 특정 Exception들을 설정해주고 있다.
여러개의 Exception을 잡아야한다면, @ExceptionHandler({IOException.class})처럼 포괄적인게 아닌 @ExceptionHandler({FileSystemException.class, RemoteException.class})로 구체적으로 명시해주는 것을 권장한다고 한다.</p>
<blockquote>
<p>We generally recommend that you be as specific as possible in the argument signature, reducing the potential for mismatches between root and cause exception types. Consider breaking a multi-matching method into individual @ExceptionHandler methods, each matching a single specific exception type through its signature.
출처 - <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-exceptionhandler">스프링 공식 문서</a></p>
</blockquote>
<h2 id="controlleradvice">@ControllerAdvice</h2>
<h3 id="controlleradvice에서-exceptionhandler-사용">@ControllerAdvice에서 @ExceptionHandler 사용</h3>
<p>@ControllerAdvice는 @Controller와 handler에서 발생하는 에러들을 모두 잡아준다.
@ControllerAdvice안에서 @ExceptionHandler를 사용하여 에러를 잡을 수 있다.</p>
<pre><code class="language-java">@ControllerAdvice
public class ExceptionHandlers {

    @ExceptionHandler(FileNotFoundException.class)
    public ResponseEntity handleFileException() {
        return new ResponseEntity(HttpStatus.BAD_REQUEST);
    }
}</code></pre>
<h4 id="범위-설정">범위 설정</h4>
<p>@ControllerAdvice는 모든 에러를 잡아주기 때문에 일부 에러만 처리하고 싶을 경우에는 따로 설정을 해주면 된다. 방법은 아래와 같다.</p>
<ul>
<li>어노테이션</li>
<li>basePackages (+basepackagesClasses)</li>
<li>assignableTypes</li>
</ul>
<pre><code class="language-java">// 어노테이션
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// basePackages
@ControllerAdvice(&quot;org.example.controllers&quot;)
public class ExampleAdvice2 {}

// assignableTypes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}</code></pre>
<p><strong>주의 사항</strong>
어노테이션, basePackages 등 설정자들은 runtime시 수행되기 때문에 너무 많은 설정자들을 사용하면 성능이 떨어질 수 있다!
<a href="https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html">스프링 공식 문서</a></p>
<h2 id="restcontrolleradvice">@RestControllerAdvice</h2>
<p>@RestControllerAdvice는 @ControllerAdvice와 @ResponseBody을 가지고 있다.
@Controller처럼 작동하며 @ResponseBody를 통해 객체를 리턴할 수 있다.</p>
<pre><code class="language-java">@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
    // ...    
}</code></pre>
<h3 id="controlleradvice-vs-restcontrolleradvice">@ControllerAdvice vs @RestControllerAdvice</h3>
<p>@ControllerAdvice는 @Componenet 어노테이션을 가지고 있어 컴포넌트 스캔을 통해 스프링 빈으로 등록된다.</p>
<p>@RestControllerAdvice는 @Controlleradvice와 @ResponseBody 어노테이션으로 이루어져있고 HTML 뷰 보다는 Response body로 값을 리턴할 수 있다.</p>
<h4 id="주의-사항❗❗"><strong>주의 사항❗❗</strong></h4>
<p>ExceptionHandler가 붙은 함수는 꼭 protected / private 처리를 해줘야 한다.
외부에서 함수를 부르게 되면 그대로 에러 객체를 리턴하기 때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Filter, Interceptor의 차이]]></title>
            <link>https://velog.io/@eunhye_/Spring-Filter-Interceptor%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@eunhye_/Spring-Filter-Interceptor%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Wed, 22 Mar 2023 12:55:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/eunhye_/post/1a41626b-fbc7-4a40-91b4-8d4295406cdd/image.png" alt=""></p>
<h3 id="filter란">Filter란?</h3>
<ul>
<li>웹 애플리케이션에서 들어오는 요청과 나가는 응답을 가로채고 수정하는 데 사용됨</li>
<li>DispatcherServlet이 실행되기 전에 실행되며 웹 컨테이너에서 관리됨</li>
<li>Interceptor와는 달리 Request, Response 객체를 수정할 수 있음 (아래 Filter Interface 참고)</li>
<li>Java Servlet API의 표준 기능</li>
<li>인증, 로깅, XSS 방어, 이미지 및 데이터 압축, 인코딩/디코딩 요청 및 응답, 스프링과 무관하게 전역적으로 처리해야하는 작업을 처리하는데 사용됨</li>
</ul>
<h3 id="interceptor란">Interceptor란?</h3>
<ul>
<li>DispatcherServlet이 컨트롤러를 호출하기 전/후로 스프링컨 텍스트 내부에서 컨트롤러에 관한 요청과 응답에 대해 처리</li>
<li>스프링 컨텍스트 외부에 존재하는 필터와는 달리 인터셉터는 스프링 컨텍스트 내부에 존재하기 때문에 스프링의 모든 빈 객체에 접근이 가능</li>
<li>HandlerInterceptor 인터페이스를 사용하여 정의할 수 있음</li>
<li>세부적인 보안 및 인증, 인가 작업, 로깅, Controller에 넘겨주는 데이터 가공</li>
</ul>
<h3 id="filter-메서드">Filter 메서드</h3>
<pre><code class="language-java">package javax.servlet;

import java.io.IOException;

public interface Filter {

    public default void init(FilterConfig filterConfig) throws ServletException {}

    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;

    public default void destroy() {}
}</code></pre>
<p>👉 init()</p>
<p>필터 인스턴스 초기화 메서드</p>
<p>서블릿 컨테이너는 필터를 인스턴스화 한 후 한 번 init 메서드를 호출</p>
<p>👉 doFilter()</p>
<p>헤더, 매개변수 또는 속성을 추가하거나 수정하는 등 요청 및 응답 객체를 수정할 수 있음 (Interceptor는 객체 수정 못함)</p>
<p>FilterChain을 사용하여 체인의 다음 필터 또는 요청을 처리하는 서블릿을 호출할 수 있음</p>
<p>👉 destroy()</p>
<p>서블릿 컨테이너는 doFilter메서드 내의 모든 쓰레드가 종료되거나 설정한 제한 시간이 지난 후에 destroy 메서드를 한 번 호출하며, 이후에는 해당 필터의 인스턴스로는 doFilter를 호출하지 않는다.</p>
<h3 id="filter-구현-예시">Filter 구현 예시</h3>
<pre><code class="language-java">@WebFilter(urlPatterns = {&quot;/api/*&quot;}, asyncSupported = true)
@Component
@RequiredArgsConstructor
public class AccessLogFilter extends OncePerRequestFilter {

    private final AccessLogger accessLogger;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        long stime = System.currentTimeMillis();
        try {
            chain.doFilter(request, response);
        } finally {
            long etime = System.currentTimeMillis();
            long elapsed = etime - stime;
            accessLogger.log(request, response, elapsed);
        }
    }
}</code></pre>
<h3 id="interceptor-메서드">Interceptor 메서드</h3>
<pre><code class="language-java">public interface HandlerInterceptor {

   default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
         throws Exception {

      return true;
   }

   default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
         @Nullable ModelAndView modelAndView) throws Exception {
   }

   default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
         @Nullable Exception ex) throws Exception {
   }

}</code></pre>
<p>👉 preHandle()</p>
<p>요청이 컨트롤러에 의해 처리되기 전에 호출됨
요청을 계속 처리해야 하는지 또는 중지해야 하는지를 나타내는 boolean 값을 리턴 (false면 작업 중단)</p>
<p>👉 postHandle()</p>
<p>요청이 컨트롤러에 의해 처리된 후 view가 렌더링되기 전에 호출됨
model과 view를 수정하는데 사용 가능</p>
<p>👉 afterCompletion()</p>
<p>뷰가 렌더링된 후 호출됨</p>
<p>로깅, 리소스 반환과 같은 정리 작업을 수행하는 데 사용</p>
<h3 id="jwt-인증-interceptor-구현-예시">JWT 인증 Interceptor 구현 예시</h3>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class JwtTokenInterceptor implements HandlerInterceptor {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        System.out.println(&quot;JwtToken 호출&quot;);
        String accessToken = request.getHeader(&quot;ACCESS_TOKEN&quot;);
        System.out.println(&quot;AccessToken:&quot; + accessToken);
        String refreshToken = request.getHeader(&quot;REFRESH_TOKEN&quot;);
        System.out.println(&quot;RefreshToken:&quot; + refreshToken);

        if (jwtTokenProvider.isValidAccessToken(accessToken) &amp;&amp; jwtTokenProvider.isValidRefreshToken(refreshToken)) {
            return true;
        }else{
            throw new RequestException(ErrorCode.JWT_BAD_TOKEN_401);
        }
    }

}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL 조인(Join) 수행 원리 (NL Join, Sort Merge Join, Hash Join)]]></title>
            <link>https://velog.io/@eunhye_/SQL-%EC%A1%B0%EC%9D%B8Join-%EC%88%98%ED%96%89-%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@eunhye_/SQL-%EC%A1%B0%EC%9D%B8Join-%EC%88%98%ED%96%89-%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Fri, 17 Mar 2023 13:56:48 GMT</pubDate>
            <description><![CDATA[<h3 id="조인-수행-원리">조인 수행 원리</h3>
<p>SQL에서 조인이란 두 개 이상의 테이블을 하나의 집합으로 만드는 연산을 뜻합니다. FROM 절에 두 개 이상의 테이블이 나열될 경우 조인이 수행되며, 조인 연산은 두 테이블 사이에서 수행됩니다.
FROM 절에 A, B, C라는 세 개의 테이블이 존재하더라도 세 개의 테이블이 동시에 조인이 수행되는 것은 아니고, 세 개의 테이블 중에서 먼저 두 개의 테이블에 대해 조인이 수행됩니다. 이러한 작업은 FROM 절에 나열된 모든 테이블을 조인할 때까지 반복 수행합니다.</p>
<h2 id="nl-joinnested-loop-join">NL Join(Nested Loop Join)</h2>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/098b3246-35f0-4e46-9b9c-71094e41f38e/image.png" alt="">
NL Join은 프로그래밍에서 사용하는 중첩된 반복문과 유사한 방식으로 조인을 수행합니다. 반복문의 외부에 있는 테이블을 선행 테이블 또는 외부 테이블 (Outer Table)이라고 하고, 반복문의 내부에 있는 테이블을 후행 테이블 또는 내부 테이블(Inner Table)이라고 합니다.</p>
<ol>
<li><p>선행 테이블에서 조건을 만족하는 첫 번째 행을 스캔
(이 때 선행 테이블에 주어진 조건을 만족하지 않는 경우 해당 데이터는 필터링)</p>
</li>
<li><p>선행 테이블의 조인 키를 가지고 후행 테이블에 조인키가 존재하는지 스캔</p>
</li>
</ol>
<p>-&gt; 조인 시도
3. 후행 테이블의 인덱스에 선행 테이블의 조인 키가 존재하는지 확인
-&gt; 선행 테이블의 조인 값이 후행 테이블에 존재하지 않으면 선행 테이블 데이터는 필터링 (더 이상 조인 작업을 진행할 필요 없음)</p>
<ol start="4">
<li>인덱스에서 추출한 레코드 식별자를 이용하여 후행 테이블을 액세스</li>
</ol>
<p>-&gt; 인덱스 스캔을 통한 테이블 액세스. 후행 테이블에 주어진 조건까지 모두 만족하면 해당 행을 추출버퍼에 넣음</p>
<h3 id="코드예시">코드예시</h3>
<pre><code class="language-sql">SELECT   /*+ USE_NL (B) */
        A.*
      , B.*
FROM    ITEM A, UITEM B
WHERE A.ITEM_ID=B.ITEM_ID --- 1
AND A.ITEM_TYPE_CD = &#39;100100&#39; --- 2
AND A.SALE_YN = &#39;Y&#39;  --- 3
AND B.SALE_YN = &#39;Y&#39;  --- 4
</code></pre>
<p>동작순서 : 2 -&gt; 3 -&gt; 1 -&gt; 4 </p>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/0800e685-3cc4-45a8-8798-32d07a1947c3/image.png" alt=""></p>
<h3 id="특징">특징</h3>
<ol>
<li><p>절차적이며, 프로그래밍에서 FOR, WHILE문 과 같은 구조로 수행된다.</p>
</li>
<li><p>선행테이블은 풀스캔하므로, 선행테이블의 크기가 작을수록 유리하다
(So. 두 테이블의 크기 차이가 있는 경우, 유리하게 사용될 수 있는 방법임)</p>
</li>
<li><p>후행테이블에 대해서는 반드시 인덱스가 존재해야 NL 조인이 가능하다. </p>
</li>
<li><p>인덱스 구성 전략이 특히 중요하다. 조인 컬럼에 대한 인덱스가 있느냐 없느냐, 있다면 컬럼이 어떻게 구성됐느냐에 따라 조인 효율이 크게 달라진다.</p>
</li>
<li><p>랜덤 액세스 방식으로 데이터를 읽는다.-&gt; 처리 범위가 좁은 것이 유리하다.</p>
</li>
</ol>
<p>이런 여러가지 특징을 종합할 때, NL 조인은 소량의 데이터를 주로 처리하거나 부분범위처리가 가능한 온라인 트랜잭션 환경에 적합한 조인 방식이라고 할 수 있다.</p>
<h2 id="sort-merge-join">Sort Merge Join</h2>
<p>Sort Merge Join은 조인 컬럼을 기준으로 데이터를 정렬하여 조인을 수행합니다. NL Join은 주로 랜덤 액세스 방식으로 데이터를 읽는 반면 Sort Merge Join은 주로 스캔 방식으로 데이터를 읽습니다. Sort Merge Join은 랜덤 액세스로 NL Join에서 부담이 되던 넓은 범위의 데이터를 처리할 때 이용되던 조인 기법입니다. 그러나 Sort Merge Join은 정렬할 데이터가 많아 메모리에서 모든 정렬 작업을 수행하기 어려운 경우에는 임시 영역(디스크)을 사용하기 때문에 성능이 떨어질 수 있습니다.</p>
<p>일반적으로 대량의 조인 작업에서 정렬 작업을 필요로 하는 Sort Merge Join 보다는 CPU 작업 위주로 처리하는 Hash Join이 성능상 유리합니다. 그러나 Sort Merge Join은 Hash Join 과는 달리 동등 조인뿐만 아니라 비동등 조인에 대해서도 조인 작업이 가능하다는 장점이 있습니다.
<img src="https://velog.velcdn.com/images/eunhye_/post/60b80d9f-e301-4943-9ca8-a8b37a89a93a/image.png" alt=""></p>
<ol>
<li><p>선행 테이블에서 주어진 조건을 만족하는 행을 찾음</p>
</li>
<li><p>선행 테이블의 조인 키를 기준으로 정렬작업을 수행</p>
<ul>
<li>1 ~ 2번 작업을 선행 테이블의 조건을 만족하는 모든 행에 대해 반복 수행</li>
</ul>
</li>
<li><p>후행 테이블에서 주어진 조건을 만족하는 행을 찾음</p>
</li>
<li><p>후행 테이블의 조인 키를 기준으로 정렬 작업을 수행</p>
<ul>
<li>3 ~ 4번 작업을 후행 테이블의 조건을 만족하는 모든 행에 대해 반복 수행</li>
</ul>
</li>
<li><p>정렬된 결과를 이용하여 조인을 수행하며 조인에 성공하면 추출버퍼에 넣음</p>
</li>
</ol>
<h3 id="코드예시-1">코드예시</h3>
<pre><code class="language-sql">SELECT  /*+ ORDERED USE_MERGE(B) */
        A.*
      , B.*
FROM  ITEM A, UITEM B
WHERE A.ITEM_ID=B.ITEM_ID   -- 1
AND A.ITEM_TYPE_CD = &#39;100101&#39;   -- 2 
AND A.SALE_YN = &#39;Y&#39;  -- 3 
AND B.SALE_YN = &#39;Y&#39;  -- 4</code></pre>
<p>동작 순서 2 -&gt; 3 -&gt; 4 -&gt; 1 
<img src="https://velog.velcdn.com/images/eunhye_/post/24e94e76-07d5-4c28-bbf1-75fcdaaa5997/image.png" alt=""></p>
<h3 id="특징-1">특징</h3>
<ol>
<li><p>인덱스가 없어도 가능한 조인법</p>
</li>
<li><p>대부분 해시조인인 보다 느린 성능을 보이나, 아래와 같은 상황에서는 소트머지 조인이 유용하다.</p>
</li>
</ol>
<ul>
<li>First 테이블에 소트연산을 대체할 인덱스가 있을 때</li>
<li>조인할 First 집합이 이미 정렬되어 있을 때</li>
<li>조인 조건식이 등차(=)조건이 아닐 때</li>
</ul>
<ol start="3">
<li>두 테이블의 사이즈가 비슷한경우에 유리하며, 사이즈 차이가 큰 경우에는 불리하고, 비효율적인 방법이다.</li>
</ol>
<h2 id="hash-join">Hash Join</h2>
<p>Hash Join은 해싱 기법을 이용하여 조인을 수행합니다. 조인될 두 테이블 중 하나를 해시 테이블로 선정하여 조인될 테이블의 조인 키 값을 Hash 알고리즘으로 비교하여 매치되는 결과값을 얻는 방식입니다.
HASH JOIN은 비용 기반 옵티마이저를 사용할 때만 사용될 수 있는 조인 방식이며 &#39;=&#39; 비교를 통한 조인에서만 사용될 수 있습니다. 주로 많은 양의 데이터를 조인해야 하는 경우에 주로 사용됩니다.</p>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/377d8aa3-819c-4a0c-82c8-cd51723b60d3/image.png" alt=""></p>
<ol>
<li><p>선행 테이블에서 주어진 조건을 만족하는 행을 찾음</p>
</li>
<li><p>선행 테이블의 조인 키를 기준으로 해시함수를 적용하여 해시 테이블을 생성 -&gt; 조인 컬럼과 SELECT 절에서 필요로 하는 컬럼도 함께 저장</p>
<ul>
<li>1 ~ 2번 작업을 선행 테이블의 조건을 만족하는 모든 행에 대해 반복 수행</li>
</ul>
</li>
<li><p>후행 테이블에서 주어진 조건을 만족하는 행을 찾음</p>
</li>
<li><p>후행 테이블의 조인 키를 기준으로 해시 함수를 적용하여 해당 버킷을 찾음 -&gt; 조인 키를 이용해서 실제 조인될 데이터를 찾음</p>
</li>
<li><p>조인에 성공하면 추출버퍼에 넣음</p>
<ul>
<li>3 ~ 5번 작업을 후행 테이블의 조건을 만족하는 모든 행에 대해 반복 수행</li>
</ul>
</li>
</ol>
<h3 id="특징-2">특징</h3>
<ol>
<li><p>조인 칼럼의 인덱스를 사용하지 않기 때문에 조인 칼럼의 인덱스가 존재하지 않을 경우데도 사용할 수 있는 기법이다.</p>
</li>
<li><p>메모리 사용이 큰 대용량 테이블 조인시 메모리 외에 임시영역(PGA 메모리)까지 사용하여 저장할 수 있어 유리함</p>
</li>
<li><p>해쉬 함수를 이용하여 조인을 수행하기 때문에 &#39;=&#39;로 수행하는 조인에서만 사용가능 합니다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security]]></title>
            <link>https://velog.io/@eunhye_/Spring-Security</link>
            <guid>https://velog.io/@eunhye_/Spring-Security</guid>
            <pubDate>Wed, 15 Mar 2023 13:13:47 GMT</pubDate>
            <description><![CDATA[<h2 id="스프링-시큐리티란">스프링 시큐리티란?</h2>
<p>Spring 기반의 어플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다. Spring Security는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행하며, 인가 과정에서 해당 리소스에 접근 권한이 있는지 확인하게 된다. 기본적으로 쿠키-세션 방식을 사용한다.</p>
<blockquote>
<p>인증(Authentication) : 해당 사용자가 본인이 맞는지 확인하는 과정
인가(Authorization) : 해당 사용자가 요청하는 자원을 실행할 수 있는 권한이 있는가를 확인하는 과정</p>
</blockquote>
<p>간단한 예시로는 인증은 ID와 PW를 통해 로그인하는 행위, 인가는 인증된 사용자가 어떠한 자원에 접근할 수 있는지를 확인하는 절차.</p>
<h3 id="securityfilterchain">SecurityFilterChain</h3>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/3264f7c1-3a5f-4943-981a-dbafd93a9769/image.png" alt=""></p>
<p>스프링 시큐리티를 이용하면 개발시에 필요한 사용자의 인증, 권한, 보안 처리를 간단하지만 강력하게 구현 할 수 있다. 일반적인 웹 환경에서 브라우저가 서버에게 요청을 보내게 되면, DispatcherServlet(FrontController)가 요청을 받기 이전에 많은 ServletFilter(서블릿 필터)거치게 된다. Security와 관련한 서블릿 필터도 실제로는 연결된 여러 필터들로 구성 되어 있다. 이러한 모습때문에 Chain(체인)이라는 표현을 쓴다.</p>
<h3 id="스프링-시큐리티-처리과정">스프링 시큐리티 처리과정</h3>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/09f60faf-7d63-469a-8bca-2e577fe5c836/image.png" alt=""></p>
<ol>
<li>사용자가 로그인 정보와 함께 인증 요청을 한다.(Http Request)</li>
<li>AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해 UsernamePasswordAuthenticationToken의 인증용 객체를 생성한다.(Authentication 인터페이스의 구현체다.)</li>
<li>AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken 객체를 전달한다.</li>
<li>AuthenticationManager는 등록된 AuthenticationProvider(들)을 조회하여 인증을 요구한다.</li>
<li>실제 DB에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨준다.</li>
<li>넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만든다.</li>
<li>AuthenticationProvider(들)은 UserDetails를 넘겨받고 사용자 정보를 비교한다.</li>
<li>인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환한다.</li>
<li>다시 최초의 AuthenticationFilter에 Authentication 객체가 반환된다.</li>
<li>Authenticaton 객체를 SecurityContext에 저장한다.</li>
</ol>
<p>최종적으로 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장한다. 그 후 인가 처리를 진행한다.</p>
<h3 id="userdetails">UserDetails</h3>
<pre><code class="language-java">public class PrincipalDetails implements UserDetails {

    private User user;

    public PrincipalDetails(User user) {
        this.user = user;
    }

    //해당 User의 권한을 리턴하는 곳!!
    //인가 처리를 위해 있음.
    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        Collection&lt;GrantedAuthority&gt; collect = new ArrayList&lt;&gt;();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });
        return collect;
    } 
    ...
}</code></pre>
<ol>
<li>시큐리티가 login 주소요청이 오면 낚아채서 로그인을 진행시킨다</li>
<li>로그인을 진행이 완료가 되면 시큐리티 session을 만들어준다.(Security ContextHolder)</li>
<li>시큐리티 세션 오브젝트 안에 Authentication 타입 객체</li>
<li>Authentication 안에 User정보가 있어야 되는데 스프링 시큐리티에서는 User정보를 UserDetails타입 객체로 받는다.</li>
</ol>
<p>Security Session(Authentication(UserDetails)) 이런 형태가 된다.</p>
<h3 id="userdetailsservice">UserDetailsService</h3>
<pre><code class="language-java">@Service
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    //시큐리티 session(내부 authentication(내부 UserDetaitls))
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(()-&gt;
                new UsernameNotFoundException(&quot;해당 username이 없습니다&quot;));
        return new PrincipalDetails(user);
    }
}</code></pre>
<p>로그인 처리시 DB에서 실제 유저 정보를 받아 UserDetails객체를 만들어 준다.</p>
<h3 id="인가">인가</h3>
<pre><code class="language-java">@Configuration
@EnableWebSecurity//스프링 시큐리티 필터가 스프링 필터체인에 등록이 됩니다.
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)// secured 어노테이션 활성화, preAuthorized 어노테이션 활성화
public class SecurityConfig {

    @Autowired
    private PrincipalOauth2UserService principalOauth2UserService;
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers(&quot;/user/**&quot;).authenticated()
                .antMatchers(&quot;/manager.**&quot;).access(&quot;hasRole(&#39;ROLE_ADMIN&#39;) or hasRole(&#39;ROLE_MANAGER&#39;)&quot;)
                .antMatchers(&quot;/admin/**&quot;).access(&quot;hasRole(&#39;ROLE_ADMIN&#39;)&quot;)
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage(&quot;/loginForm&quot;)
                //.usernameParameter(&quot;username2&quot;) -&gt; userDetailsService의 loadByUsername함수 파라미터값을 바꾸고싶을때
                .loginProcessingUrl(&quot;/login&quot;)
                .defaultSuccessUrl(&quot;/&quot;)
        return http.build();
    }
}</code></pre>
<p>스프링 시큐리티 필터에서 인가를 설정할 수 있다.</p>
<pre><code class="language-java">@Secured(&quot;ROLE_ADMIN&quot;)
    @GetMapping(&quot;/info&quot;)
    public @ResponseBody String info(){
        return &quot;개인정보&quot;;
    }

    @PreAuthorize(&quot;hasRole(&#39;ROLE_MANAGER&#39;) or hasRole(&#39;ROLE_ADMIN&#39;)&quot;)
    @GetMapping(&quot;/data&quot;)
    public @ResponseBody String data(){
        return &quot;data정보&quot;;
    }</code></pre>
<p><em>Refference</em>
<a href="https://velog.io/@jm3128/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0">스프링시큐리티_jm3128</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Layered Architecture ]]></title>
            <link>https://velog.io/@eunhye_/Layered-Architecture</link>
            <guid>https://velog.io/@eunhye_/Layered-Architecture</guid>
            <pubDate>Tue, 14 Mar 2023 13:51:07 GMT</pubDate>
            <description><![CDATA[<h2 id="layered-architecturen-tier-architecture란">Layered Architecture(N-tier Architecture)란?</h2>
<p>말 그대로 계층이 있는 아키텍쳐.
각 계층은 수평적으로 구성이 되고, 각 계층마다 특정 역할을 한다.</p>
<p>보통은 아래의 4계층으로 구성된다.</p>
<ul>
<li>Presentation</li>
<li>Bussiness</li>
<li>Persistence</li>
<li>Database</li>
</ul>
<p>하지만 Application의 규모에 따라서 3- 5 개 등의 구조를 가질 수 있음.</p>
<p>AOP처럼 횡단관심사로 분리해서 수평 계층으로 구성되어 각각의 layer는 하나의 관심사만 집중할 수 있도록 하는 것이다. 
<img src="https://velog.velcdn.com/images/eunhye_/post/828316cc-9656-45ea-92e8-b91f8bb5c598/image.png" alt=""></p>
<h3 id="주요-개념">주요 개념</h3>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/e3a954eb-503c-4644-bee8-588b4cea5070/image.png" alt="">
Layered Architecture에서는 각 계층마다 화살표를 통해 흐름을 이어간며 각 계층을 뛰어넘을 수 없다. ( Presentation → Persistence)</p>
<h4 id="왜-뛰어넘을-수-없을까">왜 뛰어넘을 수 없을까?</h4>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/layered&quot;)
@RequiredArgsConstructor
public class TestController {
    private final TestRepository testRepository;
    private final TestService testService;

    @Transactional
    @GetMapping
    public Test getUserInfo(TestReqDto reqDto) {
        Optional&lt;Test&gt; byId = testRepository.findById(1L);
        Test test = byId.orElse(new Test());
        test.updateUserInfo(reqDto);
        return testService.getUserinfo();
    }
}

@Entity
@Table(name = &quot;test&quot;)
public class Test {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // 고유번호 PK
    private String name; // 이름
    private int age; // 나이
    private String gender; // 성별
    private String email; // 이메일
    private String contact; // 연락처

    public void updateUserInfo(TestReqDto updateDto) {
        this.name = updateDto.getName();
        this.gender = updateDto.getGender();
        this.email = updateDto.getEmail();
        this.contact = updateDto.getContact();
    }
}</code></pre>
<p>위의 코드와 같이 Controller에서 Domain 간의 관계가 밀접하게 연관이 되고, 변경사항이 Service와 Repository에 퍼지게 된다.
+추후 수정도 어려워진다.</p>
<p>때문에 각 계층은 서로에게 관심을 두지 않고 필요에 의해서 찾는 관계가 되어야한다.</p>
<h4 id="예시">예시</h4>
<p><img src="https://velog.velcdn.com/images/eunhye_/post/29ca3a85-b915-4b77-9e67-408e935102de/image.png" alt=""></p>
<h3 id="표현계층-presentation-layer">표현계층 (Presentation Layer)</h3>
<p>사용자의 요청에 대해서 해석, 응답하는 계층</p>
<ul>
<li>로깅</li>
<li>인증(보안)</li>
</ul>
<p>ex) Controller</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/layered&quot;)
@RequiredArgsConstructor
public class TestController {
        private final TestService testService;

    @GetMapping
    public Test updateUser(TestReqDto reqDto) {
        return testService.updateUserInfo(reqDto);
    }
}</code></pre>
<h3 id="응용-프로그램-계층bussiness-layer">응용 프로그램 계층(Bussiness Layer)</h3>
<p>실질적인 비즈니스 로직을 처리하는 계층. (보통 Service를 뜻 함.)</p>
<p><strong>도메인</strong>(Domain Layer)과 <strong>인프라스트럭쳐</strong>(InfraStructure Layer)를 연결</p>
<pre><code class="language-java">// 인터페이스
public interface TestService {
    D_Test updateUserInfo(TestReqDto reqDto);
}

// 구현체
@Service
@RequiredArgsConstructor
public class TestServiceImpl implements TestService {
    private final TestRepository repository;
    @Override
    public Test updateUserInfo(TestReqDto reqDto) {
                if(reqDto.getId() == null || reqDto.getId() == 0) {
                    throw new CommonException(&quot;존재하지 않는 아이디 입니다.&quot;);
                }
        return repository.findById(reqDto.getId()).orElse(null);
    }
}</code></pre>
<h3 id="영속성-계층persistence-layer">영속성 계층(Persistence Layer)</h3>
<p>DB에 접근하는 계층. Business에서 온 요청으로 조회 함.</p>
<p>DDD에서 쓸 때에는 도메인 레이어라고 부르는 것 같음</p>
<h3 id="도메인-계층domain-layer">도메인 계층(Domain Layer)</h3>
<blockquote>
<p>도메인 : 소프트웨어로 해결하고자 하는 문제 영역 </p>
</blockquote>
<p>예를 들어 쇼핑몰이 있다면 쇼핑몰 자체가 하나의 도메인이 되고,
그 아래 </p>
<ul>
<li><p>고객 (Customer)</p>
</li>
<li><p>판매자 (Seller)</p>
</li>
<li><p>주문 (Order)</p>
</li>
<li><p>포인트 (Point)</p>
</li>
<li><p>결제 (Payment)</p>
</li>
<li><p>배송 (Delivery)
등등 하위 도메인으로 나눌 수 있다.</p>
<pre><code class="language-java">@Entity
@Table(name = &quot;domain_test&quot;)
public class DomainTest {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id; // 고유번호 PK
  private String name; // 이름
  private int age; // 나이
  private String gender; // 성별
  private String email; // 이메일
  private String contact; // 연락처

  public void updateUserInfo(TestReqDto updateDto) {
      this.name = updateDto.getName();
      this.gender = updateDto.getGender();
      this.email = updateDto.getEmail();
      this.contact = updateDto.getContact();
  }
}</code></pre>
</li>
</ul>
<h3 id="layered-architecture의-장점">Layered Architecture의 장점</h3>
<ul>
<li>배우기 쉬움</li>
<li>느슨한 연결을 이용해, 자신의 관심사만 집중할 수 있음</li>
<li>비즈니스 로직을 유지해서 유지보수와 확장성 측면에서 이득을 얻을 수 있음.</li>
<li>재사용성 높음</li>
</ul>
<h3 id="layered-architecture의-단점">Layered Architecture의 단점</h3>
<ul>
<li>서비스가 커질수록 복잡도가 증가해 확장성이 떨어짐</li>
<li>싱크홀 안티패턴을 고려해야 함.</li>
</ul>
<p><em>Refference</em>
<a href="https://velog.io/@jeb1225/DDD%EC%9D%98-%EA%B3%84%EC%B8%B5%EA%B5%AC%EC%A1%B0Layered-architecture">https://velog.io/@jeb1225/DDD의-계층구조Layered-architecture</a>
<a href="https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html">https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 더티 체킹(Dirty Checking)]]></title>
            <link>https://velog.io/@eunhye_/JPA-%EB%8D%94%ED%8B%B0-%EC%B2%B4%ED%82%B9</link>
            <guid>https://velog.io/@eunhye_/JPA-%EB%8D%94%ED%8B%B0-%EC%B2%B4%ED%82%B9</guid>
            <pubDate>Wed, 08 Mar 2023 05:18:28 GMT</pubDate>
            <description><![CDATA[<h3 id="더티-체킹dirty-checking-이란">더티 체킹(Dirty Checking) 이란?</h3>
<p>더티체킹은 Transaction 안에서 엔티티의 변경이 일어나면, 변경 내용을 자동으로 데이터베이스에 반영하는 JPA 특징이다.</p>
<p>Dirty Checking의 뜻은 변경 감지 정도로 생각할 수 있다. 즉, 변경을 감지해서 DB에 반영한다.</p>
<p>예를 들어 다음과 같은 코드가 있다.</p>
<pre><code class="language-java">@Slf4j
@RequiredArgsConstructor
@Service
public class PayService {

    public void updateNative(Long id, String tradeNo) {
        EntityManager em = entityManagerFactory.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin(); //트랜잭션 시작
        Pay pay = em.find(Pay.class, id);
        pay.changeTradeNo(tradeNo); // 엔티티만 변경
        tx.commit(); //트랜잭션 커밋
    }
}</code></pre>
<p>코드를 보면 별도로 데이터베이스에 save 하지 않는다.</p>
<ol>
<li>트랜잭션이 시작되고</li>
<li>엔티티를 조회하고</li>
<li>엔티티의 값을 변경하고</li>
<li>트랜잭션을 커밋합니다.</li>
</ol>
<p>여기서 데이터베이스에 update 쿼리에 관한 코드는 어디에도 없다.</p>
<p>이 코드가 어떻게 작동하는지 테스트 코드를 작성하여 수행해보면,</p>
<pre><code class="language-java">@RunWith(SpringRunner.class)
@SpringBootTest
public class PayServiceTest {

    @Autowired
    PayRepository payRepository;

    @Autowired
    PayService payService;

    @After
    public void tearDown() throws Exception {
        payRepository.deleteAll();
    }

    @Test
    public void 엔티티매니저로_확인() {
        //given
        Pay pay = payRepository.save(new Pay(&quot;test1&quot;,  100));

        //when
        String updateTradeNo = &quot;test2&quot;;
        payService.updateNative(pay.getId(), updateTradeNo);

        //then
        Pay saved = payRepository.findAll().get(0);
        assertThat(saved.getTradeNo()).isEqualTo(updateTradeNo);
    }
}</code></pre>
<p>아래와 같은 로그를 확인할 수 있다.
<img src="https://velog.velcdn.com/images/eunhye_/post/cc56fd46-ee89-44b4-9912-59a0fb750c1e/image.png" alt="">
save 메소드로 변경 사항을 저장하지 않았음에도 update 쿼리가 실행되었다.</p>
<p>JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해준다.</p>
<p>이때 변화가 있다의 기준은 최초 조회 상태이다.</p>
<p>JPA에서는 엔티티를 조회하면 해당 엔티티의 조회 상태 그대로 스냅샷을 만들어놓는다.
그리고 트랜잭션이 끝나는 시점에는 이 스냅샷과 비교해서 다른점이 있다면 Update Query를 데이터베이스로 전달한다. 
이런 상태 변경 검사의 대상은 영속성 컨텍스트가 관리하는 엔티티에만 적용 된다.</p>
<blockquote>
<p>detach된 엔티티 (준영속)
DB에 반영되기 전 처음 생성된 엔티티 (비영속)</p>
</blockquote>
<p>준영속/비영속 상태의 엔티티는 Dirty Checking 대상에 포함되지 않기 때문에 값을 변경해도 데이터베이스에 반영되지 않는다.</p>
<p>Spring Data Jpa와 @Transactional이 함께 할 경우엔 다음과 같다.</p>
<pre><code class="language-java">@Slf4j
@RequiredArgsConstructor
@Service
public class PayService {

    private final PayRepository payRepository;

    @Transactional
    public void update(Long id, String tradeNo) {
        Pay pay = payRepository.getOne(id);
        pay.changeTradeNo(tradeNo);
    }
}</code></pre>
<p>위 코드의 테스트 코드를 작성해서 실행해보면</p>
<pre><code class="language-java">@Test
public void SpringDataJpa로_확인() {
    //given
    Pay pay = payRepository.save(new Pay(&quot;test1&quot;,  100));

    //when
    String updateTradeNo = &quot;test2&quot;;
    payService.update(pay.getId(), updateTradeNo);

    //then
    Pay saved = payRepository.findAll().get(0);
    assertThat(saved.getTradeNo()).isEqualTo(updateTradeNo);
}</code></pre>
<p>아래와 같이 정상적으로 update 쿼리가 수행됨을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/eunhye_/post/2eb05f98-d071-4ed4-8d42-81b74901dc21/image.png" alt=""></p>
<h3 id="변경-부분만-update하고-싶을땐-dynamicupdate">변경 부분만 update하고 싶을땐? @DynamicUpdate</h3>
<p>Dirty Checking으로 생성되는 update 쿼리는 기본적으로 모든 필드를 업데이트한다.</p>
<p>JPA에서는 전체 필드를 업데이트하는 방식을 기본값으로 사용하며,
전체 필드를 업데이트하는 방식의 장점은 다음과 같다.</p>
<ul>
<li>생성되는 쿼리가 같아 부트 실행시점에 미리 만들어서 재사용가능합니다.</li>
<li>데이터베이스 입장에서 쿼리 재사용이 가능하다<ul>
<li>동일한 쿼리를 받으면 이전에 파싱된 쿼리를 재사용한다.</li>
</ul>
</li>
</ul>
<p><em>출처: 김영한님의 자바 ORM 표준 JPA 프로그래밍</em></p>
<p>다만, 필드가 20~30개 이상으로 많은 경우엔 이런 전체 필드 Update 쿼리가 부담스러울 수 있다.
<em>(사실 이런 경우 정규화가 잘못된 경우일 확률이 높다.
한 테이블에 필드 30개는 확실히 많다.)</em></p>
<p>그래서 이런 경우엔 @DynamicUpdate로 변경 필드만 반영되도록 할 수 있다.</p>
<p>엔티티 최상단에 아래와 같이 @DynamicUpdate 를 선언해주시면 된다.</p>
<pre><code class="language-java">@Getter
@NoArgsConstructor
@Entity
@DynamicUpdate // 변경한 필드만 대응
public class Pay {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String tradeNo;
    private long amount;</code></pre>
<p>그리고 다시 테스트 코드를 수행해서 로그를 확인해보면!
<img src="https://velog.velcdn.com/images/eunhye_/post/eae845c5-4504-4084-9a0b-cfafda18fe86/image.png" alt=""></p>
<p>변경분 (trade_no)만 Update 쿼리에 반영된 것을 확인할 수 있다.</p>
<p><em>Referrence</em>
<a href="https://jojoldu.tistory.com/415">더티 체킹 (Dirty Checking)이란? by 향로</a></p>
]]></description>
        </item>
    </channel>
</rss>