<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jione.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sat, 19 Jul 2025 13:52:14 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jione.log</title>
            <url>https://velog.velcdn.com/images/ji-one/profile/54494c10-101c-4df0-8663-32e711ed0ff4/image.gif</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jione.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ji-one" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Health Check가 서킷 브레이커를 대체할 수 있을까?]]></title>
            <link>https://velog.io/@ji-one/Health-Check%EA%B0%80-%EC%84%9C%ED%82%B7-%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%BB%A4%EB%A5%BC-%EB%8C%80%EC%B2%B4%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@ji-one/Health-Check%EA%B0%80-%EC%84%9C%ED%82%B7-%EB%B8%8C%EB%A0%88%EC%9D%B4%EC%BB%A4%EB%A5%BC-%EB%8C%80%EC%B2%B4%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Sat, 19 Jul 2025 13:52:14 GMT</pubDate>
            <description><![CDATA[<p>서킷 브레이커에 대해 얘기를 나누다가 &#39;쿠버네티스가 Health Check로 파드(Pod)를 알아서 격리하고 재시작해주는데, 굳이 서킷 브레이커(Circuit Breaker)를 별도로 구축해야 할까?&#39; 라는 의문이 들었다.</p>
<p>결론적으로, 두 기술은 목적과 역할이 달라 서로를 대체할 수 없으며 함께 사용될 때 비로소 진정한 안정성을 확보할 수 있는 상호 보완적인 관계다. 이 글에서는 두 기술의 역할을 명확히 구분하고 함께 사용했을 때의 이점에 대해 다루고자 한다.
<br/></p>
<h2 id="liveness--readiness-probes">Liveness &amp; Readiness Probes</h2>
<p>먼저 쿠버네티스가 제공하는 기본적인 헬스 체크 기능인 프로브(Probe)에 대해 알아보자.</p>
<h3 id="liveness-probe">Liveness Probe</h3>
<ul>
<li>목적: 컨테이너가 살아있는지 확인</li>
<li>실패 시: 컨테이너 재시작</li>
<li>교착 상태(Deadlock)에 빠져 애플리케이션이 멈췄지만, 프로세스는 종료되지 않은 경우 감지하여 자동 복구할 때 사용</li>
</ul>
<h3 id="readiness-probe">Readiness Probe</h3>
<ul>
<li>목적: 컨테이너가 요청을 처리할 준비가 되었는지 확인</li>
<li>실패 시: 서비스의 로드 밸런서 대상에서 제외하여 새로운 트래픽을 보내지 않음</li>
<li>애플리케이션 시작 시 대용량 데이터를 로딩하거나, 외부 서비스와 연결하는 등 내부적으로 준비 과정이 필요할 때 사용</li>
</ul>
<br/>

<p>이 프로브들은 컨테이너의 상태를 HTTP GET 요청, TCP 소켓 연결, 내부 명령어 실행(exec) 이라는 세 가지 기준으로 판단하며             initialDelaySeconds, periodSeconds, failureThreshold 등의  설정값으로 동작을 세밀하게 제어할 수 있다. 또한 Liveness와 Readiness 프로브는 서로 독립적이고 병렬로 동작한다.</p>
<br/>


<h2 id="circuit-breaker">Circuit Breaker</h2>
<p>서킷 브레이커는 특정 서비스로의 요청이 계속 실패하면 말 그대로 회로를 끊어버려 추가적인 요청 시도를 막는 패턴이다.</p>
<p>서킷 브레이커는 세 가지 상태를 갖는다.</p>
<h3 id="closed">Closed</h3>
<ul>
<li>정상 상태</li>
<li>모든 요청이 정상적으로 전달</li>
</ul>
<h3 id="open">Open</h3>
<ul>
<li>장애 상태</li>
<li>요청 실패율이 임계치를 넘으면 회로가 열림</li>
<li>이후 들어오는 요청은 실제 서비스로 전달되지 않고 즉시 실패 처리(Fail-Fast)</li>
<li>이를 통해 클라이언트의 자원 낭비를 막고 장애 전파 방지</li>
</ul>
<h3 id="half-open">Half-Open</h3>
<ul>
<li>복구 확인 상태</li>
<li>회로가 열린 후 일정 시간이 지나면, 소수의 테스트용 요청을 보내 서비스가 복구되었는지 확인하고 성공 시 회로 Close, 실패 시 다시 Open</li>
</ul>
<br/>

<h2 id="health-check-vs-circuit-breaker">Health Check vs. Circuit Breaker</h2>
<p>두 기술의 차이를 이해하기 위해, 주문 서비스가 결제 서비스를 호출하는 상황에서 결제 서비스의 DB에 장애가 발생한 경우를 가정해보자.</p>
<h3 id="health-check의-관점">Health Check의 관점</h3>
<p>기본적인 Readiness Probe는 웹 서버의 응답 여부만 확인하므로, 결제 서비스의 파드 자체는 정상 구동 중으로 판단한다. 따라서 쿠버네티스는 결제 서비스가 건강하다고 보고, 주문 서비스의 요청을 계속해서 전달한다.</p>
<p>하지만 결제 서비스는 DB 장애로 인해 모든 요청을 처리하지 못하고 타임아웃이나 500 에러를 반환한다. 이로 인해 주문 서비스는 응답 없는 요청을 계속 대기하게 되어 가용 스레드를 모두 소진하게 되고 이는 연쇄 장애(Cascading Failure)로 이어질 수 있다.</p>
<h3 id="circuit-breaker의-관점">Circuit Breaker의 관점</h3>
<p>주문 서비스에 구현된 서킷 브레이커는 결제 서비스로의 요청 실패율 급증을 즉시 감지하고, 회로를 Open 상태로 전환한다.</p>
<p>이후 주문 서비스는 더 이상 결제 서비스로 네트워크 요청을 보내지 않고, 즉시 에러를 반환하거나 미리 정의된 대체 로직(Fallback)을 수행한다. 이를 통해 불필요한 자원 낭비를 막고 장애의 연쇄적인 전파를 차단하여 시스템 전체를 보호한다.</p>
<br/>

<p>그렇다면 <strong>Readiness Probe에서 외부 DB 연결까지 확인하도록 설정하면 되지 않을까?</strong> 그러나 이는 회복 과정에서 서킷 브레이커와 결정적인 차이가 발생한다.</p>
<p>Readiness Probe의 경우 DB가 복구되는 순간, 모든 파드가 거의 동시에 Readiness Probe에 성공한다. 그 결과, 쿠버네티스는 대기 중이던 모든 클라이언트의 요청 트래픽을 갓 회복된 서비스로 한꺼번에 쏟아붓게 된다. 이로 인해 서비스는 다시 장애 상태에 빠질 수 있는데 이를 Thundering Herd 문제라고 한다.</p>
<p>반면, 서킷 브레이커는 Half-Open 상태가 존재한다. 즉 회로가 열린 후 소수의 테스트 요청만으로 서비스의 실제 복구 여부를 확인하고 이후 회로를 닫아 전체 트래픽을 정상화시키므로 시스템에 주는 부담이 적다.</p>
<br/>

<h2 id="핵심-차이점-비교">핵심 차이점 비교</h2>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">Kubernetes Probes (Liveness/Readiness)</th>
<th align="left">Circuit Breaker</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>관점</strong></td>
<td align="left"><strong>서버(호출받는 쪽)</strong>의 관점</td>
<td align="left"><strong>클라이언트(호출하는 쪽)</strong>의 관점</td>
</tr>
<tr>
<td align="left"><strong>보호 대상</strong></td>
<td align="left">개별 파드 자신</td>
<td align="left">클라이언트 애플리케이션과 전체 시스템</td>
</tr>
<tr>
<td align="left"><strong>판단 기준</strong></td>
<td align="left">파드 내부의 상태 (프로세스, 포트, 스크립트 실행)</td>
<td align="left">외부 서비스의 응답 상태 (성공/실패율, 지연 시간)</td>
</tr>
<tr>
<td align="left"><strong>핵심 기능</strong></td>
<td align="left">파드 재시작 또는 트래픽 격리</td>
<td align="left">즉시 실패(Fail-Fast) 및 장애 전파 방지, 점진적 회복</td>
</tr>
</tbody></table>
<br/>

<h2 id="마치며">마치며</h2>
<p>쿠버네티스의 Liveness/Readiness Probe는 개별 파드의 상태를 관리하는 인프라 레벨의 기능이다.</p>
<p>반면, 서킷 브레이커는 서비스 간의 의존성 관계에서 장애 전파를 방지하는 애플리케이션 레벨의 디자인 패턴이다.</p>
<p>따라서 두 기술은 선택의 문제가 아니다. 기본적인 안정성은 Health Check으로 확보하고 서비스 간 의존성이 높은 환경에서는 서킷 브레이커를 함께 구축하여 더욱 견고하고 회복탄력성 있는 아키텍처를 지향해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[기본 설정을 신뢰하지 말고, 명시적으로 설정하자 (스레드 풀, 타임아웃)]]></title>
            <link>https://velog.io/@ji-one/%EA%B8%B0%EB%B3%B8-%EC%84%A4%EC%A0%95%EC%9D%84-%EC%8B%A0%EB%A2%B0%ED%95%98%EC%A7%80-%EB%A7%90%EA%B3%A0-%EB%AA%85%EC%8B%9C%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%84%A4%EC%A0%95%ED%95%98%EC%9E%90-%ED%83%80%EC%9E%84%EC%95%84%EC%9B%83-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%92%80</link>
            <guid>https://velog.io/@ji-one/%EA%B8%B0%EB%B3%B8-%EC%84%A4%EC%A0%95%EC%9D%84-%EC%8B%A0%EB%A2%B0%ED%95%98%EC%A7%80-%EB%A7%90%EA%B3%A0-%EB%AA%85%EC%8B%9C%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%84%A4%EC%A0%95%ED%95%98%EC%9E%90-%ED%83%80%EC%9E%84%EC%95%84%EC%9B%83-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%92%80</guid>
            <pubDate>Thu, 17 Jul 2025 14:27:38 GMT</pubDate>
            <description><![CDATA[<p>최근 비동기(<code>@Async</code>) 기능의 요청이 처리되지 않는 문제가 있었다. 두 가지의 주요 설정 누락이 문제의 원인이었고 이 글에서는 기본 설정에 의존했을 때 발생하는 문제점과 명시적 설정의 중요성에 대해 다루고자 한다.</p>
<h2 id="원인-1-spring-boot의-기본-threadpooltaskexecutor-사용">원인 1. Spring Boot의 기본 ThreadPoolTaskExecutor 사용</h2>
<p>문제가 되는 기능은 <code>@Async</code>을 통해 비동기로 처리되고 있다. 우리 서버에는 별도의 Executor를 지정해주지 않았고, 이 경우 Spring Boot는 기본 스레드 풀인 TaskPoolTaskExecutor를 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/ji-one/post/b1b14124-2490-4cec-9d07-d11691bfbee2/image.png" alt=""></p>
<ul>
<li>출처: <a href="https://docs.spring.io/spring-boot/docs/2.5.2/reference/htmlsingle/#features.task-execution-and-scheduling">https://docs.spring.io/spring-boot/docs/2.5.2/reference/htmlsingle/#features.task-execution-and-scheduling</a><br/>


</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ji-one/post/6f198479-1320-44e6-92c3-4ed07404e5ed/image.png" alt="">
기본 ThreadPoolTaskExecutor의 설정은 다음과 같다.</p>
<blockquote>
<ul>
<li>corePoolSize: 8</li>
</ul>
</blockquote>
<ul>
<li>maxPoolSize: Integer.MAX_VALUE</li>
<li>queueCapacity: Integer.MAX_VALUE</li>
</ul>
<br/>

<p>문제는 기본 설정의 queueCapacity가 사실상 무제한이라는 점이다. 이로 인해 큐가 가득 차는 상황이 발생하지 않아 스레드 풀은 절대 corePoolSize인 8개를 초과하여 확장되지 않는다. 만약 8개의 코어 스레드가 모두 블로킹 작업으로 점유되면, 이후의 모든 비동기 작업은 스레드를 할당받지 못하고 무한정 큐에 쌓이게 된다. 이렇게 스레드 풀이 고갈된 것이 문제의 원인 중 하나이다.</p>
<p><img src="https://velog.velcdn.com/images/ji-one/post/b5bb10a7-69df-49be-a8e4-d7b2533c9fdb/image.png" alt=""></p>
<p>따라서 반드시 TaskExecutor Bean을 직접 등록하여 상황에 맞게 스레드 풀 설정을 명시적으로 관리해야 한다. 위와 같이 queueCapacity를 유한한 값으로 설정하고, corePoolSize와 maxPoolSize를 적절히 조절하여 스레드 풀이 유연하게 확장되도록 구성해야 한다.</p>
<br/>


<h2 id="원인-2-타임아웃-부재">원인 2. 타임아웃 부재</h2>
<p>두 번째 원인은 외부 서비스와 통신하는 gRPC 클라이언트 호출에 타임아웃이 설정되어 있지 않았다.</p>
<p><img src="https://velog.velcdn.com/images/ji-one/post/91d7370d-8f57-4deb-80e2-f5e941eff9f0/image.png" alt=""></p>
<ul>
<li>출처: <a href="https://grpc.io/docs/guides/deadlines/#deadlines-on-the-client">https://grpc.io/docs/guides/deadlines/#deadlines-on-the-client</a></li>
</ul>
<p>공식 문서의 내용처럼 gRPC 클라이언트는 디폴트로 Deadline이 설정되어있지 않다. 이는 클라이언트가 서버의 응답을 무한정 기다리는 상태에 빠질 수 있음을 의미한다.</p>
<p>문제 발생 시점 외부 gRPC 서버의 응답 시간이 급격히 지연되고 있었다. 결론적으로 타임아웃이 없는 gRPC 호출로 인해 corePoolSize인 8개의 스레드를 모두 블로킹 상태로 만들었고 새로운 비동기 작업을 처리할 수 없는 상태로 빠진 것이 이번 문제의 핵심 원인이다.</p>
<p><img src="https://velog.velcdn.com/images/ji-one/post/6b88e087-bd37-48dd-9300-2f64853c9552/image.png" alt=""></p>
<ul>
<li>출처: <a href="https://www.baeldung.com/spring-webflux-timeout#1-response-timeout">https://www.baeldung.com/spring-webflux-timeout#1-response-timeout</a><br/>

</li>
</ul>
<p>gRPC뿐만 아니라, Netty 기반으로 동작하는 WebClient 역시 기본적으로 응답 타임아웃이 설정되어 있지 않으므로 반드시 명시적으로 지정해야 한다.</p>
<p>타임아웃이 없으면 연동 서비스의 응답이 느릴 때 처리량이 급격히 떨어진다. 더 나아가, 응답을 대기하면서 시스템 자원을 고갈 시켜 전체 기능의 장애로 전파될 수 있다.</p>
<p>따라서 다음과 같은 효과를 위해 타임아웃 설정은 필수적이다.</p>
<ul>
<li><p>장애 격리: 외부 서비스의 문제가 우리 시스템으로 전파되는 것을 차단</p>
</li>
<li><p>Fail-Fast: 사용자가 무한정 대기하거나 반복적인 새로고침으로 서버 부하를 가중시키는 것보다, 빠르게 실패를 인지하고 에러 화면을 보여주는 것이 훨씬 낫다.</p>
</li>
</ul>
<br/>

<h2 id="결론">결론</h2>
<p>이처럼 gRPC 클라이언트의 타임아웃, @Async 스레드 풀의 동작 방식처럼, &quot;설정하지 않음&quot;은 종종 &quot;무한대기&quot; 또는 &quot;무제한&quot;을 의미할 수 있다.</p>
<p>이번 이슈는 외부 서비스 호출과 내부 비동기 처리에서 기본 설정을 그대로 사용한 것이 원인이었기에
안정적인 서비스를 구축하기 위해 주요 설정 값들을 기본값에 의존하지 말고, 반드시 운영 환경의 특성을 고려하여 명시적으로 설정해야 한다는 것을 배웠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka 리텐션 정책 (Delete/Compact) 살펴보기]]></title>
            <link>https://velog.io/@ji-one/Kafka-%EB%A6%AC%ED%85%90%EC%85%98-%EC%A0%95%EC%B1%85-DeleteCompact-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ji-one/Kafka-%EB%A6%AC%ED%85%90%EC%85%98-%EC%A0%95%EC%B1%85-DeleteCompact-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 21 Jun 2025 09:16:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ji-one/post/234168d4-d8e1-48a1-adf2-f89a60329c62/image.png" alt=""></p>
<p>최근 신규 프로젝트를 배포한 후, AWS MSK의 디스크 사용량이 급증하는 문제가 생겼다. 예상보다 훨씬 빠른 속도로 특정 토픽들에 메시지가 쌓이고 있는 것이 원인이었고 다행히 해당 토픽의 데이터는 장기간 보관할 필요가 없어서 <code>retention.ms</code>을 1시간으로 변경하는 것으로 대응했다. 이 과정은 Kafka의 리텐션 정책을 다시 돌아보는 계기가 됐다. MSK 대시보드의 수많은 설정 중, 이번 글에서는 데이터의 생명 주기와 직결되는 Retention 관련 설정에 대해서만 집중적으로 다뤄보려고 한다.</p>
<p>Kafka에서 데이터가 얼마나 오래 저장될지는 리텐션 정책(Retention Policy)에 의해 결정된다.
<code>cleanup.policy</code> 설정값에 따라 크게 두 가지로 나눌 수 있다.</p>
<h3 id="cleanuppolicydelete"><code>cleanup.policy=delete</code></h3>
<p>default 설정값이다. 설정된 시간(<code>retention.ms</code>)이나 크기(<code>retention.bytes</code>)를 초과한 데이터를 물리적으로 삭제한다.</p>
<ul>
<li><code>retention.ms</code>: 데이터가 보관될 최대 시간 (default: 7일). 이 시간을 초과한 로그 세그먼트는 삭제 대상이다.</li>
<li><code>retention.bytes</code>: 각 파티션이 가질 수 있는 최대 크기 (default: 10GB). 파티션 크기가 이 값을 초과하면 가장 오래된 로그 세그먼트부터 삭제된다.</li>
</ul>
<p>그리고 위 두 설정값은 OR 조건으로 동작한다. Log Manager의 삭제 스레드가 <code>log.retention.check.interval.ms</code> 마다 실행되면서 둘 중 하나의 조건이라도 만족하면 데이터 삭제 작업을 수행한다.</p>
<ul>
<li><code>log.retention.check.interval.ms</code>: 리텐션 정책(삭제 대상)을 확인할 백그라운드 스레드의 실행 간격</li>
</ul>
<p>kafka는 메시지를 하나하나 지우는 방식이 아니라, 로그 세그먼트 파일 단위로 이루어지기 때문에 retention은 segment 설정과도 밀접한 관련이 있다.
<code>retention.ms</code>가 1일로 설정되어있어도, 특정 메시지가 1일이 지났다고해서 즉시 삭제되는 것이 아니라 해당 메시지가 포함된 <strong>세그먼트 전체</strong>가 삭제 조건을 만족해야 파일이 삭제된다. 그래서 <code>segment.bytes</code> 설정이 매우 크다면 우리의 예상보다 데이터가 오래 남아있을 수 있기 때문에 해당 설정도 함께 검토해야 한다.</p>
<h3 id="cleanuppolicycompact"><code>cleanup.policy=compact</code></h3>
<p>메시지 키를 기준으로 각 키의 가장 마자막 값만 유지하는 정책이다. 키가 동일한 새로운 메시지가 들어오면 이전 메시지는 삭제(압축) 대상이 된다. 따라서 데이터의 최종 상태를 추적하는 용도에 유용하다.</p>
<p><code>cleanup.policy</code>가 <code>compact</code> 단독으로 설정된 경우에는<code>retetnion.ms</code>, <code>retention.bytes</code> 설정은 아무런 역할을 하지 않는다. 이 정책의 목표는 &#39;오래된 데이터 삭제&#39;가 아니라 &#39;Key 별로 최신 데이터만 남기는 것&#39;이기 때문에.</p>
<p>나는 이 정책의 동작 원리가 궁금해서 찾아봤다. 로그 세그먼트 파일을 직접 수정 하는 것인가? 이는 Kafka의 모든 로그 세그먼트는 불변이기 때문에 불가능하다.</p>
<p>동작 원리는 다음과 같다.
<code>compact</code>는 Kafka 브로커의 백그라운드 스레드인 로그 클리너(Log Cleaner)가 오래된 세그먼트를 읽어 필요한 데이터만 남긴 새로운 세그먼트를 만든 후, 기존 세그먼트를 통째로 교체하는 방식으로 동작한다.
조금 더 자세히 말하자면, <code>log.cleaner.backoff.ms</code> (default: 15초) 주기로 로그 클리너가 깨어나 압축 대상을 찾고 <code>min.cleanable.dirty.ratio</code> (default: 0.5) 설정을 확인하고, dirty 부분의 비율이 이 값을 넘으면 압축 시작을 준비한다. 로그 클리너는 오직 비활성 세그먼트들만을 대상으로 압축을 수행한다.</p>
<ul>
<li><code>Active Segment</code>: 현재 새로운 메시지가 쓰이고 있는 가장 마지막 세그먼트 파일. 활성 세그먼트는 절대 압축 대상이 되지 않는다.</li>
<li><code>Inactive Segments</code>: 더 이상 데이터가 쓰이지 않고 닫힌 이전 세그먼트 파일. </li>
</ul>
<p>또한, 로그는 논리적으로 Clean/Dirty 부분으로 나뉜다.</p>
<ul>
<li><code>Clean</code>: 압축되어 모든 키에 대해 유일한 값만 존재하는 로그의 앞부분</li>
<li><code>Dirty</code>: 아직 압축되지 않아 키 중복 있을 수 있는 로그의 뒷부분</li>
</ul>
<p>따라서 <strong><code>Dirty Ratio</code> = 전체 로그의 바이트(Byte) 크기 / Dirty 부분의 바이트(Byte) 크기</strong>이다.</p>
<p>해당 비율은 로그 클리너가 너무 자주, 그리고 너무 적은 양의 데이터를 처리하느라 자원(CPU, I/O)을 낭비하는 것을 막기 위한 장치이며 Clean과 Dirty의 경계는 세그먼트 파일 단위가 아니라 특정 오프셋(Offset)이다.</p>
<p>로그 클리너는 Dirty 로그를 처음부터 끝까지 읽고, {Key, 마지막 메시지의 Offset} 형태의 인메모리 맵을 생성한다. 따라서 스캔이 끝나면, 각 키의 가장 마지막 버전이 어떤 오프셋에 있는지 알 수 있다.
이후 새로운 <strong>임시 세그먼트 파일(.cleaned)</strong>을 만들고, 다시 원본 세그먼트들을 읽으면서 읽고 있는 메시지의 {Key, Offset} 맵에 기록된 값과 일치하면, 그 메시지는 최신 값이므로 새로운 .cleaned 세그먼트 파일에 복사한다. 
모든 복사가 끝나면, Kafka는 원본 세그먼트 파일을 새로 만든 .cleaned 세그먼트 파일로 원자적(atomic)으로 교체하고 기존의 낡은 세그먼트 파일은 삭제된다.</p>
<p>다이아그램으로 보면 아래와 같다.</p>
<pre><code>[원본 비활성 세그먼트 파일]
(offset 10, key: A, value: v1)
(offset 11, key: B, value: v1)
(offset 12, key: A, value: v2)
        |
        |  &lt;--- 로그 클리너 스레드가 처리 (키의 마지막 오프셋을 추적하는 인메모리 맵 생성)
        |
[새로운 .cleaned 임시 세그먼트 파일 생성]
(offset 11, key: B, value: v1)  // 복사
(offset 12, key: A, value: v2)  // 복사
        |
        |  &lt;--- 작업 완료 후 원자적으로 교체 (Swap)
        |
[새로운 압축된 세그먼트 파일]
(offset 11, key: B, value: v1)
(offset 12, key: A, value: v2)</code></pre><p>그렇다면 Compact 정책에서 특정 키의 데이터를 완전히 삭제하려면 어떻게 해야할까?
해당 키에 value가 null인 메시지를 보내면 된다. 이 메시지를 Tombstone(묘비석)이라 부른다. (Tomb는 무덤이라는 뜻) 
로그 클리너는 이 Tombstone을 보고 해당 키의 모든 이전 데이터를 삭제하며, Tombstone 자체도 <code>delete.retention.ms</code> (default: 24시간)가 지나면 삭제한다. 이는 모든 컨슈머가 키 삭제 이벤트를 인지할 시간을 보장하기 위함이다.</p>
<p>키의 값이 빠르게 변경될 때, 이전 값이 너무 빨리 압축되어 사라지는 것을 막기 위한 <code>min.compaction.lag.ms</code> 라는 설정도 있다. 메시지가 토픽에 기록된 후 최소한 이 시간 동안은 압축되지 않도록 보장한다. 따라서 모든 상태 변화를 순서대로 처리해야 하는 컨슈머를 보호할 수 있게 된다. (delete 정책에서는 의미 없는 설정)</p>
<h3 id="compact-delete-모두-설정-하이브리드-방식"><code>compact, delete 모두 설정 (하이브리드 방식)</code></h3>
<p>cleanup.policy에 compact, delete 두 가지를 모두 설정하는 것도 가능하다. 이 경우 Kafka는 먼저 압축(compact)을 통해 키별 최신 값만 남긴 후, 그 데이터에 대해 삭제(delete) 정책(retention.ms/bytes)을 적용하여 오래된 데이터를 최종적으로 삭제한다.</p>
<h3 id="정리"><code>정리</code></h3>
<p><img src="https://velog.velcdn.com/images/ji-one/post/ea25a838-dc1a-445c-b522-d662d555cea0/image.png" alt=""></p>
<p>Gemni가 지금까지의 내용을 표로 간단하게 정리해줬다. 이번 경험을 통해 Kafka의 리텐션 정책을 명확히 구분할 수 있게 되었다. 시간/크기 기반으로 데이터를 삭제하는 delete와, 키(Key)를 기준으로 최신 값만 남기는 compact가 완전히 다른 설정과 동작 방식을 가진다는 것을 이해했다. 이제는 데이터의 특성에 따라 정책 및 관련된 세부 설정들을 고려해볼 수 있을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MYSQL - SELECT와 동시에 INSERT하기]]></title>
            <link>https://velog.io/@ji-one/MYSQL-SELECT%EC%99%80-%EB%8F%99%EC%8B%9C%EC%97%90-INSERT-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ji-one/MYSQL-SELECT%EC%99%80-%EB%8F%99%EC%8B%9C%EC%97%90-INSERT-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 06 Dec 2022 14:44:59 GMT</pubDate>
            <description><![CDATA[<h1 id="select-insert">SELECT INSERT</h1>
<blockquote>
<p>MYSQL에서 일부 테이블의 데이터를 조회한 결과를 바로 다른 테이블에 INSERT시킬 수 있다.</p>
</blockquote>
<h2 id="두-테이블의-컬럼이-같을-때">두 테이블의 컬럼이 같을 때</h2>
<pre><code class="language-sql">INSERT INTO [테이블명] 
SELECT * FROM [조회 테이블명]</code></pre>
<p>✅ 뒤에 WHERE 절을 추가해 조건을 입력할 수도 있다.</p>
<h2 id="두-테이블의-컬럼이-일치하지-않을-때">두 테이블의 컬럼이 일치하지 않을 때</h2>
<pre><code class="language-sql">INSERT INTO [테이블명] (컬럼명1, 컬럼명2, ...)
SELECT 컬럼명1, 컬럼명2, ... FROM [조회 테이블명]</code></pre>
<p>✅ 조회되는 컬럼과 INSERT 될 컬럼의 데이터 형식이 일치해야 한다.</p>
<br/>

<h1 id="select-into">SELECT INTO</h1>
<blockquote>
<p>SELECT INSERT는 테이블의 검색 결과를 이미 존재하는 테이블에 삽입해준다. 
<strong>이에 반해 SELECT INTO는 INTO 문 다음에 지정한 테이블을 새로 생성시켜서 삽입해준다.</strong></p>
</blockquote>
<pre><code class="language-sql">SELECT * INTO [생성될 테이블명] FROM [조회 테이블명]</code></pre>
<p>✅ 생성될 테이블명은 반드시 존재하지 않는 테이블이어야 한다.</p>
<h1 id="참고">참고</h1>
<ul>
<li><a href="https://jh-tr.tistory.com/17">https://jh-tr.tistory.com/17</a></li>
<li><a href="http://www.webmadang.net/database/database.do?action=read&amp;boardid=4001&amp;page=1&amp;seq=25">http://www.webmadang.net/database/database.do?action=read&amp;boardid=4001&amp;page=1&amp;seq=25</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DTO vs VO vs Entity]]></title>
            <link>https://velog.io/@ji-one/DTO-vs-VO-vs-Entity</link>
            <guid>https://velog.io/@ji-one/DTO-vs-VO-vs-Entity</guid>
            <pubDate>Sun, 04 Dec 2022 14:59:34 GMT</pubDate>
            <description><![CDATA[<h1 id="dto">DTO</h1>
<blockquote>
<p><code>DTO</code>는 <code>Data Transfer Object</code>로 계층(Layer)간 데이터 교환을 위해 사용하는 객체다. </p>
</blockquote>
<p>여기서 계층은 View, Controller, Service, Repository 등을 의미한다. 데이터 교환을 위해서만 사용되기에 로직은 갖고 있지 않고 데이터를 담고 꺼내는 getter/setter 메소드만 갖는다. <u>DTO는 데이터를 전송할 때 사용되는 바구니라고 생각하면 좋다.</u></p>
<ul>
<li><p><strong>DTO 예제 코드</strong></p>
<pre><code class="language-java">class MoveDto {
    private String source;
    private String target;

    public MoveDto(int rend, int green, int blue) {
        this.source = source;
        this.target = target;
    }

    public String getSource() {
        return source;
    }

    public String getTarget() {
        return target;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public void setTarget(String target) {
        this.target = target;
    }
}</code></pre>
</li>
</ul>
<h1 id="vo">VO</h1>
<blockquote>
<p><code>VO</code>는 <code>Value Object</code>로 값 그 자체를 표현하는 객체이다. </p>
</blockquote>
<p><u>서로 다른 이름을 갖는 VO 인스턴스라도 모든 속성 값이 같다면 두 인스턴스는 같은 객체라고 할 수 있다.</u> 이를 위해 VO는 <code>equals()</code>와 map 등에서 hashCode로 값을 찾기 위해 <code>hashCode()</code>를 필수적으로 오버라이딩 해야한다. 따라서 VO의 모든 속성 값이 같다면 같은 객체이다의 전제 조건은 equals()와 hashCode()를 오버라이딩하는 것이다. 
VO는 객체의 불변성을 보장하며 DTO와 달리 로직을 포함할 수 있다. 또한 DTO와 다르게 VO는 값 그 자체의 의미를 가진 <code>불변 클래스(Read-Only)</code>를 의미한다. DTO는 인스턴스 개념이라면 VO는 리터럴 개념이라고 할 수 있다.</p>
<h1 id="dto-vs-vo">DTO vs VO</h1>
<ul>
<li><code>DTO</code>는 Layer간의 통신 용도로 오고가는 객체이다.</li>
<li><code>VO</code>는 특정한 값을 나타내는 객체이다.</li>
<li>웹 개발에서 혼용해서 쓰는 VO는 사실 DTO다. </li>
</ul>
<h1 id="entity">Entity</h1>
<blockquote>
<p><code>Entity</code>는 실제 DB 테이블과 매핑되는 클래스이다. </p>
</blockquote>
<p>속성 값으로 구분되는 VO와 달리 Entity는 ID로 구분된다. 로직을 포함할 수 있다. Entity는 JPA에서 사용하는 객체이므로 JPA 외에서 사용하지 않는 것을 권장한다.</p>
<ul>
<li><p><strong>Entity 예제</strong></p>
<pre><code class="language-java">@Table(&quot;user_info&quot;)
public class UserInfo {

    @Id
    private Long id;
    private String name;
    private String email;

    public UserInfo() {}

    public UserInfo(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public Long getId() { return this.id; }

    public String getName() { return this.name; }

    public String getEmail() { return this.email; }
}</code></pre>
</li>
</ul>
<h1 id="entity를-dto-대신에-사용할-수-있을까">Entity를 DTO 대신에 사용할 수 있을까?</h1>
<p>사용할 수는 있다. 그러나 View에서 요청하는 속성 값들이 요청에 따라 계속 달라질 수가 있는데, Entity를 사용하게 되면 영속성 모델을 표현한 Entity 자체의 값의 순수성이 모호하기 때문에 <u>Controller에서 사용할 DTO와 Entity 클래스는 분리하는게 좋다. 또한, Entity 객체를 영속 계층 바깥쪽에서 사용하는 방식보다는 DTO를 사용하는게 좋다.</u></p>
<p><code>Client &lt;--DTO--&gt; Controller &lt;--DTO--&gt; Service &lt;-- DTO --&gt; Repository &lt;-- Domain (Entity) --&gt; DB</code></p>
<p>순수하게 데이터를 담고 있다는 점에선 DTO와 Entity 객체가 유사하지만 DTO는 목적 자체가 데이터의 전달이라 읽고, 쓰는 것이 모두 허용되어 일회성으로 사용되는 성격이 강하다. JPA를 이용하게 되면 Entity 객체는 단순히 데이터를 담는 객체라기 보단 실제 데이터베이스와 관련이 있고, 내부적으로 EM(Entity Manager)이 관리하는 객체다. 생명주기도 다르기 때문에 분리하여 처리한다.</p>
<h1 id="정리">정리</h1>
<table>
<thead>
<tr>
<th></th>
<th>DTO</th>
<th>VO</th>
<th>Entity</th>
</tr>
</thead>
<tbody><tr>
<td>용도</td>
<td>Layer간의 데이터 전송</td>
<td>의미있는 값을 표현</td>
<td>DB 테이블과 매핑되는 클래스</td>
</tr>
<tr>
<td>가변/불변</td>
<td>가변 객체(Mutable Object)를 생성 후 상태를 변경할 수 있다.</td>
<td>불변 객체(Immutablel Object) 생성 후 상태 변경이 없다.</td>
<td>가변 객체(Mutable Object)를 생성 후 상태를 변경할 수 있다.</td>
</tr>
<tr>
<td>로직 포함 여부</td>
<td>N</td>
<td>Y</td>
<td>Y</td>
</tr>
</tbody></table>
<h1 id="참고">참고</h1>
<ul>
<li><a href="https://www.youtube.com/watch?v=J_Dr6R0Ov8E">https://www.youtube.com/watch?v=J_Dr6R0Ov8E</a></li>
<li><a href="https://sasca37.tistory.com/2#--%--DTO%---Data%--Transfer%--Object-%C-%A-">https://sasca37.tistory.com/2#--%--DTO%---Data%--Transfer%--Object-%C-%A-</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ngrok으로 외부에서 로컬 서버 접속하기]]></title>
            <link>https://velog.io/@ji-one/ngrok%EC%9C%BC%EB%A1%9C-%EC%99%B8%EB%B6%80%EC%97%90%EC%84%9C-%EB%A1%9C%EC%BB%AC-%EC%84%9C%EB%B2%84-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ji-one/ngrok%EC%9C%BC%EB%A1%9C-%EC%99%B8%EB%B6%80%EC%97%90%EC%84%9C-%EB%A1%9C%EC%BB%AC-%EC%84%9C%EB%B2%84-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 29 Nov 2022 10:13:57 GMT</pubDate>
            <description><![CDATA[<h1 id="ngrok">ngrok</h1>
<p><a href="https://ngrok.com/">ngrok</a>은 방화벽 넘어 외부에서 로컬에 접속할 수 있게 하는 터널 프로그램이다. 보통 로컬에서 개발을 진행하기 때문에 외부에서 접근하려면 서버에 올리는 등의 작업을 해야 하므로 번거롭다.
이때, ngrok을 사용하면 따로 네트워크 환경 설정 변경 없이 로컬에 실행 중인 서버를 외부에서 접근할 수 있도록 할 수 있다.</p>
<h1 id="설치">설치</h1>
<ul>
<li><p><a href="https://ngrok.com/download">ngrok - download</a></p>
</li>
<li><p>macOS의 경우 <code>brew</code>로 설치 가능</p>
<pre><code class="language-bash">  brew install ngrok/ngrok/ngrok</code></pre>
</li>
</ul>
<h1 id="사용법">사용법</h1>
<ul>
<li><p>기본적으로 세션 유지 기간은 8시간이다. 제한 없이 사용하고 싶다면 ngrok 사이트 회원가입 후 인증 토큰을 발급받으면 된다.</p>
<pre><code class="language-bash">ngrok config add-authtoken [발급받은 인증 token]</code></pre>
</li>
<li><p>로컬 서버 포트가 8083이라면 아래와 같이 입력 후 외부에서 접속할 수 있는 URL을 확인할 수 있다.</p>
<pre><code class="language-bash">ngrok http 8083</code></pre>
<p><img src="https://velog.velcdn.com/images/ji-one/post/19909278-53f8-440c-a9bd-8877a73ef689/image.png" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[무료 메모앱 추천: 옵시디언(Obsidian)설치부터 사용법까지]]></title>
            <link>https://velog.io/@ji-one/9v7aqpip</link>
            <guid>https://velog.io/@ji-one/9v7aqpip</guid>
            <pubDate>Wed, 10 Aug 2022 14:21:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ji-one/post/6b8c08e9-8650-40e9-b93d-3c595c81d00c/image.png" alt=""></p>
<br>

<h1 id="0-들어가기에-앞서">0. 들어가기에 앞서</h1>
<p>!youtube[xu3XGEomRWI]
&#39;나도 글 좀 잘 써보고싶다!&#39; 생각하던 중 위 영상을 보게 되었다. 김민석 테크니컬 라이팅 코치님이 <code>1:37:00</code> 부터 효과적인 메모 작성법 및 관리법을 소개해주신다. 여기서 <code>제텔카스텐(Zettelkasten)</code>을 소개하시며 이를 구축하기 위하여 <code>옵시디언(Obsidian)</code>이라는 메모 앱을 사용하신다. </p>
<p>이 앱이 너무 좋아보였다! 설치하고 사용해보니 더 좋다!</p>
<p>옵시디언과 관련한 한국어 글이 많지 않은 것 같아 설치법 및 간단한 사용법을 블로그에 작성하고자 한다. </p>
<br/>

<h1 id="1-옵시디언obsidian-소개">1. 옵시디언(Obsidian) 소개</h1>
<blockquote>
<h2 id="a-second-brain-for-you-forever"><em>A second brain, for you, forever</em></h2>
</blockquote>
<h4 id="obsidian-is-a-powerful-knowledge-base-on-top-of-a-local-folder-of-plain-text-markdown-files"><em>Obsidian is a powerful knowledge base on top of a local folder of plain text Markdown files.</em></h4>
<p>옵시디언 공식 홈페이지 메인에 적혀있는 문구다. Obsidian을 &quot;두번째 뇌&quot;라고 소개한다. <strong>두번째 뇌는 우리 신체의 두뇌와 비슷한 시스템을 디지털 공간에 구축하는 것이다.</strong> 나의 생각과 지식들을 두 번째 뇌에 저장하게 되면 우리는 암기하고자 노력하지 않아도 된다. 또한, 필요한 정보를 찾는데 시간을 줄여주고 창의적인 활동에 시간을 더 쓸 수 있게해주는 장점이 있다.</p>
<br/>

<h2 id="11-제텔카스텐zettelkasten">1.1 제텔카스텐(Zettelkasten)</h2>
<p>두 번째 뇌를 구축하기 위한 노트 필기 방법론으로 <a href="https://en.wikipedia.org/wiki/Zettelkasten">제텔카스텐(Zettelkasten)</a>을 사용할 수 있다. 여기서 제텔카스텐에 대해 자세하게 설명하지는 않고 두 개의 특징만 소개한다.  </p>
<ul>
<li>많은 개별 노트로 구성되어 있다.</li>
<li>노트는 노트를 서로 연결할 수 있도록 메타데이터(예: 태그)를 포함할 수 있고 다른 노트를 참조할 수도 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ji-one/post/4d5a0d4d-d074-40a2-89f0-387cd0b12c8c/image.png" alt="">
옵시디언에서는 제텔카스텐의 특징처럼 많은 개별 노트로 구성되어 있고 노트와 노트를 연결할 수도 있다.
왼쪽 화면처럼 옵시디언에서 개별 노트를 작성할 때 연관된 다른 노트를 참조하게 되면 그래프를 통해 시각적으로 확인할 수 있다. 현재 노트와 참조하고 있는 노트가 선으로 연결되어 작성한 메모들의 연관관계를 한 눈에 파악할 수 있다.  <del>이 그래프가 인간 뇌처럼 뉴런을 모방한 것 같다.</del></p>
<br/>

<h2 id="12-가격">1.2 가격</h2>
<p><img src="https://velog.velcdn.com/images/ji-one/post/c995138b-f1da-4dc5-8b97-eb2d32d0401e/image.png" alt="">
상업적 목적으로 사용하는 것이 아니라면 <strong>무료</strong>다.
<strong>옵시디언은 로컬 저장소를 기반으로 동작한다.</strong> 클라우드 저장소를 사용하여 다른 기기와 동기화하고 싶다면 추가 비용을 지불해야한다. 
<strong>하지만 iCloud나 다른 third-party 서비스를 사용하면 무료로 동기화가 가능하다.</strong> 동기화하는 방법은 <code>2.1절</code>에서 소개한다. </p>
<br/>

<h2 id="13-장점">1.3 장점</h2>
<ol>
<li>무료다.</li>
<li>플러그인이 많다. 
현재 630개의 서드 파티 플러그인이 있으며 이를 통해 다양한 기능을 사용할 수 있다.</li>
<li>테마 커스터마이징이 자유롭다. CSS만 수정하면 된다.</li>
<li>기본적으로 로컬 저장소 기반이다. 
따라서 Confidential한 메모도 서버에 올라가지 않는다. 또한, 오프라인에서 사용할 수도 있다.</li>
</ol>
<br/>

<h2 id="14-단점">1.4 단점</h2>
<ol>
<li>로컬 저장소 기반으로 동작하기 때문에 다른 사람들과 메모를 공유하거나 여러명이서 작성하고자 한다면 적합하지 않을 수 있다.<ul>
<li>github나 obsidian에서 제공하는 publish 서비스(유료)를 사용한다면 공유할 수 있다. </li>
</ul>
</li>
<li>마크다운을 모르면 쓰기 어렵다.</li>
</ol>
<br/>


<h1 id="2-다운로드-및-설치">2. 다운로드 및 설치</h1>
<p><a href="https://obsidian.md/">옵시디언(Obsidian) 공식홈페이지</a>에서 본인 운영체제에 맞는 파일을 다운로드하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/ji-one/post/23736793-62fd-4211-b248-0a9138de680f/image.png" alt="">
<code>MAC OS</code> 기준 다운로드된 디스크 이미지 파일을 클릭하고 <code>Obsidian</code>을 <code>Applications</code> 폴더에 넣어주면 끝이다. 별도의 설치 과정은 없다. </p>
<br/>

<h2 id="21-pc와-모바일iphone-ipad-연동하는-법">2.1 PC와 모바일(iPhone/ iPad) 연동하는 법</h2>
<blockquote>
<p>💡 모바일이 아닌 <strong>여러 PC와 동기화</strong>하기 위해서는 아래의 third-party 서비스를 이용할 수 있다.</p>
</blockquote>
<ul>
<li>Dropbox</li>
<li>Google Drive</li>
<li>iCloud Drive</li>
<li>OneDrive</li>
<li>Syncthing</li>
</ul>
<blockquote>
<p>💡 <strong>모바일과 연동</strong>하고자 한다면, <strong>아이폰은 <code>icloud</code>, 안드로이드는 <code>Dropsync</code> 또는 <code>FolderSync</code></strong>를 이용하면 된다. 
참고: <a href="https://help.obsidian.md/Getting+started/Sync+your+notes+across+devices">https://help.obsidian.md/Getting+started/Sync+your+notes+across+devices</a></p>
</blockquote>
<p>여기서는 iCloud를 사용하여 iPhone/iPad를 연동하는 방법만 설명한다. </p>
<ol>
<li><p>App Strore에서 <code>Obsidian</code>을 다운로드 한다.</p>
</li>
<li><p>설치 후 앱을 실행시키고 vault를 생성한다. (vault는 보관함이라 생각하면 된다.)
원하는 이름을 입력하고 <code>Store in iCloud</code>를 활성화한다. 
<img src="https://velog.velcdn.com/images/ji-one/post/03fa37a3-d5c8-48a6-a43a-c4871611160b/image.png" alt=""></p>
</li>
<li><p><code>Create</code> 버튼을 클릭한다.</p>
</li>
<li><p>PC에서 Obsidian을 실행하고 <code>폴더를 보관함으로 열기</code> 우측 <code>열기</code> 버튼을 클릭한다. (한국어로 언어를 변경한 상태다.)
<img src="https://velog.velcdn.com/images/ji-one/post/e94ebc51-baf7-4cb5-b665-259866a6dccc/image.png" alt=""></p>
</li>
<li><p>파일 탐색기가 뜨면  <code>iCloud Drive &gt; Obsidian</code>으로 이동한다.
<img src="https://velog.velcdn.com/images/ji-one/post/a3dc4a2a-0332-4b43-b8ee-0c5a744f5157/image.png" alt=""></p>
</li>
<li><p><code>단계 2</code>에서 생성한 vault가 있음을 확인할 수 있다. 해당 vault를 선택하고 아래와 같은 창이 뜨면 연동이 완료된 것이다.
<img src="https://velog.velcdn.com/images/ji-one/post/4ab34621-3490-4421-beb1-5915cf8e27d4/image.png" alt=""></p>
</li>
</ol>
<br/>

<h1 id="3-사용법">3. 사용법</h1>
<p>설치까지 완료했으니 간단한 사용법을 살펴보자.</p>
<br/>

<h2 id="31-노트-및-폴더-생성">3.1 노트 및 폴더 생성</h2>
<p><img src="https://velog.velcdn.com/images/ji-one/post/16135f40-4415-4df1-9837-eb53dd795500/image.png" alt=""> </p>
<ul>
<li>노트 생성: 좌측 패널 상단 노트 아이콘을 클릭하거나 단축키 <code>Command</code> + <code>N</code> 사용</li>
<li>폴더: 좌측 패널 상단 폴더 아이콘 클릭</li>
</ul>
<p>임의로 <code>0. Inbox</code> 폴더와 <code>1. Reviews</code> 폴더를 생성했다. <code>0. Inbox</code>는 아직 정리가 덜 된 노트를 보관하는 용도이다. 
루트에 새 노트가 생성되는게 디폴트인데, Inbox 폴더 내부에 새로운 노트가 생성된다면 편할 것이다. 이를 변경하는 법은 바로 아래 <code>3.1.1절</code>에서 소개한다. </p>
<br/>

<h3 id="311-새-노트가-생성되는-폴더-변경하는-법">3.1.1 새 노트가 생성되는 폴더 변경하는 법</h3>
<ol>
<li>앱 화면 맨 좌측 하단에 <code>설정</code> 아이콘을 클릭</li>
<li><code>파일 &amp; 링크 &gt; 새로운 노트 생성 장소</code>를 <code>아래에 특정되어 있는 경로</code>로 변경</li>
<li><code>파일 &amp; 링크 &gt; 새로운 노트가 만들어질 파일</code>을 원하는 폴더로 변경
<img src="https://velog.velcdn.com/images/ji-one/post/174b51a0-1e0d-4b90-ae3f-c53428458b1c/image.png" alt=""></li>
</ol>
<br/>

<h2 id="32-템플릿-생성-및-단축키-설정">3.2 템플릿 생성 및 단축키 설정</h2>
<blockquote>
<p>💡 메모를 매번 같은 형식으로 작성하고 싶다면 템플릿으로 설정해놓을 수 있다.</p>
</blockquote>
<br/>

<h3 id="321-템플릿-생성">3.2.1 템플릿 생성</h3>
<ol>
<li>원하는 템플릿 형식을 작성
Template 폴더를 따로 생성하여 해당 폴더에 필요한 템플릿을 넣어두는 것을 추천한다.
<img src="https://velog.velcdn.com/images/ji-one/post/c681eee7-95d7-4bbd-9fd3-01bc09803dd0/image.png" alt=""></li>
<li><code>설정 &gt; 주요 플러그인</code>에서 <code>템플릿</code> 활성화</li>
<li>활성화 버튼 좌측 설정 아이콘 클릭
<img src="https://velog.velcdn.com/images/ji-one/post/f57cb49b-ccf7-47ee-8e0c-1d7fcc449367/image.png" alt=""></li>
<li><code>템플릿 폴더 경로</code>를 템플릿이 작성된 폴더로 지정
<img src="https://velog.velcdn.com/images/ji-one/post/43b0ab51-7b76-4694-816f-8793d99591ce/image.png" alt=""></li>
</ol>
<br/>

<h3 id="322-템플릿-사용법">3.2.2 템플릿 사용법</h3>
<p><img src="https://velog.velcdn.com/images/ji-one/post/857f4f65-8d0c-40a6-8412-783f1fa857af/image.png" alt="">
새 노트를 열고 앱 화면 맨 좌측에서 <code>템플릿 삽입</code> 아이콘을 선택한다. (위에서 4번째)
<img src="https://velog.velcdn.com/images/ji-one/post/a4afc192-8685-4ea0-926f-8ab33bd245d1/image.png" alt="">
지정한 템플릿이 작성된 것을 확인할 수 있다.</p>
<br/>

<h3 id="323-단축키-설정법">3.2.3 단축키 설정법</h3>
<blockquote>
<p>💡 추가로 템플릿 삽입을 단축키로 설정해두면 편리하다. </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ji-one/post/d76229c4-7638-43ed-84f6-7a82da951f0a/image.png" alt="">
단축키는 <code>설정 &gt; 단축키</code>에서 설정할 수 있다. 
템플릿 삽입을 단축키로 설정하기 위해서는 <code>템플릿: 템플릿 삽입</code>에 키 조합을 입력한다.
단축키 조합은 자유롭게 지정하면 된다. 나는 <code>Command</code>+<code>F</code>로 설정했다. 
동일한 방법으로 다른 기능의 단축키도 얼마든지 설정할 수 있다.</p>
<br/>

<h2 id="33-연결-그래프-뷰-사용">3.3 연결 그래프 뷰 사용</h2>
<br/>

<h3 id="331-노트-연결하는-법">3.3.1 노트 연결하는 법</h3>
<p><img src="https://velog.velcdn.com/images/ji-one/post/06a8a434-feda-4579-9e15-c7436018357a/image.png" alt="">
예시로 노트 <code>A</code>, <code>B</code>를 생성했다.  <code>A</code> 노트에서 <code>#연습</code> 태그를 추가하고 <code>연결문서</code>를 <code>B</code>로 작성했다. 
노트를 연결하기 위해서는 <code>[[연관 노트 이름]]</code> 형식으로 작성하면 된다. 처럼 현재 노트를 B 노트와 링크한다면 <code>[[B]]</code>로 입력한다.</p>
<br/>

<blockquote>
<p>💡 <strong>단순 노트만 링크할 수 있는 것이 아니라 특정 헤더나 블록 등을 링크할 수도 있다.</strong></p>
</blockquote>
<ul>
<li>노트 링크: <code>[[노트명]]</code></li>
<li>특정 헤더 링크: <code>[[노트명#헤더명]]</code></li>
<li>특정 블록 링크: <code>[[노트명#^]]</code> 
#^을 입력하면 하단 팝업창으로 지정 노트의 블록들이 자동으로 뜬다. 원하는 블록을 선택하기만 하면 된다.</li>
<li>파일 (이미지,mp3 등) 임베디드: <code>![[파일명]]</code></li>
<li>메모 직접 링크 (메모 내용을 불러옴): <code>![[노트명]]</code></li>
</ul>
<br/>

<h3 id="332-그래프-뷰-보는-법">3.3.2 그래프 뷰 보는 법</h3>
<blockquote>
<p>👀 노트를 연결했다면 그래프를 통해 시각적으로 확인할 수 있다. </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ji-one/post/3729cbc0-8941-417e-8ef0-77d04d7c56c7/image.png" alt="">
맨 좌측의 <code>그래프 뷰 열기</code> 아이콘(위에서 2번째)을 선택하면 아래 사진처럼 현재 vault에 존재하는 노트 간의 연관 그래프를 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/ji-one/post/b059679c-7a9a-43c3-91c6-e31cf09b841d/image.png" alt="">
<img src="https://velog.velcdn.com/images/ji-one/post/6a1f6df8-3af2-42e5-948a-d7c4d6a17846/image.png" alt="">
<code>그룹</code>을 이용하면 특정 노트들을 묶어서 구분할 수도 있다. (예: <code>연습</code> 태그를 포함하고 있는 노트는 파란색으로 표시)
<img src="https://velog.velcdn.com/images/ji-one/post/47090610-98a6-450b-a94b-b11dde90a63f/image.png" alt="">
<code>그래프 뷰</code> 창 상단의 맨 좌측 아이콘을 클릭하면 창을 끌어서 재배치가 가능하다. 
<img src="https://velog.velcdn.com/images/ji-one/post/6cd6e6f2-44bb-4432-a5a5-4cfa6a1d9c37/image.png" alt="">
메모 우측 패널에 두면 매번 <code>그래프 뷰 열기</code> 아이콘을 클릭하지 않고 메모를 작성하는 동시에 그래프를 확인할 수 있다.</p>
<br/>

<h1 id="4-마무리">4. 마무리</h1>
<p><img src="https://velog.velcdn.com/images/ji-one/post/d50dfbce-5ef0-4e22-8091-06a3e69846ab/image.jpg" alt="">
옵시디언을 소개하며 제텔카스텐, 두 번째 뇌 개념을 소개하고 설치법과 간단한 사용법까지 살펴보았다. 
옵시디언 기능 중 아주 일부만 살펴보았기 때문에 여기서 다루지 못한 것들과 사용해보면서 유용한 기능들도 블로그에 남길 것이다. 지식과 생각을 더 효율적으로 기록하기 위한 방법 역시 앞으로 고민해야한다. 
옵시디언을 통하여 메모 관리법을 익히고 꾸준히 나의 두 번째 뇌를 구축해야겠다! </p>
<br/>

<h1 id="5-참고">5. 참고</h1>
<ul>
<li><a href="https://www.buildingasecondbrain.com/">https://www.buildingasecondbrain.com/</a></li>
<li><a href="https://help.obsidian.md/Obsidian/Index">https://help.obsidian.md/Obsidian/Index</a></li>
<li><a href="https://www.youtube.com/watch?v=IGcD8hoHnu4&amp;list=PLy4SLsxzyLUUJlu0L-_U7c1jy_bqvPMR6">https://www.youtube.com/watch?v=IGcD8hoHnu4&amp;list=PLy4SLsxzyLUUJlu0L-_U7c1jy_bqvPMR6</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>