<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Gnar.log</title>
        <link>https://velog.io/</link>
        <description>💻 + ☕ = &lt;/&gt;</description>
        <lastBuildDate>Mon, 16 Dec 2024 02:35:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Gnar.log</title>
            <url>https://images.velog.io/images/_koiil/profile/68b79d56-4120-473a-998b-35cb5a1b039b/imageedit_1_4545880772.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Gnar.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/_koiil" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[JPA 인덱스 스캔 이슈]]></title>
            <link>https://velog.io/@_koiil/JPA-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%8A%A4%EC%BA%94-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@_koiil/JPA-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%8A%A4%EC%BA%94-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Mon, 16 Dec 2024 02:35:45 GMT</pubDate>
            <description><![CDATA[<h3 id="배경">배경</h3>
<p>현재의 서비스는 사용자의 활동 로그를 기반으로 개인화 추천을 제공하고 있습니다. 각 원천 서비스에서 발생하는 로그를 Hadoop으로 수집하고, 일일 배치를 통해 정제된 데이터를 서비스 DB에 적재하는 구조입니다. 해당 데이터는 정해진 정책만큼 유지되며, 이후의 데이터는 삭제됩니다.
어느 날 slow query로 인한 DB 부하 발생하여 응답 불가 상태가 되는 장애가 발생했습니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/2a5b40a0-2bdb-4644-ac8d-8262720254e9/image.png" alt=""></p>
<p>이 이슈의 원인을 크게 두가지로 파악했습니다.</p>
<ul>
<li>데이터 적재량 증가: 활동 유저가 늘어나며 해당 날짜 범위 내 데이터 자체의 사이즈가 늘어남.</li>
<li>급격한 트래픽 증가: 근 시간 내에 연결된 유저의 세션을 전부 끊어내는 작업이 있었고, 그로 인해 재접속 동선에 있던 해당 서비스에도 부하가 생김</li>
</ul>
<h3 id="원인-파악">원인 파악</h3>
<p>모니터링을 통해 분석한 결과, 다음과 같은 조회 쿼리가 문제였습니다.</p>
<pre><code class="language-sql">SELECT u.base_dt, u.class_type, u.game_id, u.member_no 
FROM user_log u 
WHERE u.member_no=191198706 
    AND u.class_type=&#39;SEARCH&#39; 
    AND u.base_dt BETWEEN &#39;2024-11-26 00:00:00&#39; AND &#39;2024-12-03 23:59:59&#39;;</code></pre>
<p>이 쿼리의 실행 계획을 확인한 결과:</p>
<pre><code class="language-shell">-&gt; Filter: ((u.class_type = &#39;SEARCH&#39;) and (u.member_no = 196821557) and (u.base_dt between &#39;2024-12-12 00:00:00&#39; and &#39;2024-12-19 23:59:59&#39;)) 
(cost=60329.56 rows=3009) 
(actual time=367.975..367.975 rows=0 loops=1) 
-&gt; Index range scan on muge1_0 using PRIMARY  (cost=60329.56 rows=3009) (actual time=367.975..367.975 rows=0 loops=1) </code></pre>
<ul>
<li>type: range</li>
<li>예상 rows: 3009</li>
<li>실제 cost가 매우 높음 (60329.56)</li>
<li>PRIMARY 키를 사용한 Index range scan 후 Filter 적용</li>
</ul>
<p>예상보다 훨씬 높은 비용이 발생하고 있었습니다. 원인을 깊게 파고들어보니 다음과 같은 문제점을 발견했습니다:</p>
<ul>
<li>데이터 타입 불일치: DB의 member_no 컬럼은 VARCHAR로 정의되어 있었지만, 애플리케이션에서는 Long 타입으로 처리<ul>
<li>collation 불일치로 인한 묵시적 형변환으로 인덱스 활용 실패</li>
</ul>
</li>
<li>fetch row 해가는 것으로 보아 cursor 기반 동작으로 예상되었고,여러 번의 비효율적인 스캔으로 커넥션을 오래 점유</li>
</ul>
<h3 id="해결">해결</h3>
<h4 id="1-긴급-조치">1. 긴급 조치</h4>
<p>우선 즉각적인 서비스 안정화를 위해 DB 커넥션 풀을 5개에서 100개로 확장했습니다.</p>
<h4 id="2-1차-확인">2. 1차 확인</h4>
<p>처음에는 JPA Specification에서 타입 변환을 시도했습니다:</p>
<pre><code class="language-java">default Specification&lt;UserLogEntity&gt; equalMemberNo(Long memberNo) {
        return (root, query, builder) -&gt; builder.equal(root.get(&quot;id&quot;).get(&quot;memberNo&quot;), memberNo.toString());
}</code></pre>
<p>해당 작업 배포 이후 쿼리를 모니터링하며 문자열로 조회 하는지 확인해봤지만 패치 내용이 반영되지 않은 것으로 확인되었습니다.</p>
<pre><code class="language-java">u.member_no=191198706 → u.member_no=&#39;191198706&#39;</code></pre>
<p>남은 쿼리 생성에 관여하는 부분은 테이블 정의로 의심하고 공식 문서를 확인했습니다.
그 결과 엔티티의 필드/프로퍼티 타입을 기준으로 매핑이 이루어진다는 내용을 확인할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/95dc3864-5cde-4e8d-abd4-08f49269bca0/image.png" alt=""></p>
<p><a href="https://jakarta.ee/specifications/persistence/3.1/jakarta-persistence-spec-3.1.html#persistent-fields-and-properties">JPA Specification에서의 Type 매핑</a></p>
<p>즉 기존 쿼리의 toString()은 SQL 생성 이전 단계에서 실행되고, Hibernate는 최종적으로 파라미터 바인딩할 때 엔티티의 필드 타입(Long)을 기준으로 다시 Type Conversion을 수행하기 때문에 의도한대로 동작하지 않은 것입니다.</p>
<p>엔티티에 정의된 필드 타입을 수정하여 재확인한 결과, 인덱스를 제대로 사용하는 것을 볼 수 있었습니다.</p>
<pre><code class="language-java">@Column(name = &quot;member_no&quot;, nullable = false, length = 30)
private String memberNo;</code></pre>
<p>수정 후 실행 계획:</p>
<pre><code class="language-shell">-&gt; Filter: ((muge1_0.class_type = &#39;SEARCH&#39;) and (muge1_0.member_no = &#39;196821557&#39;) and (muge1_0.base_dt between &#39;2024-12-12 00:00:00&#39; and &#39;2024-12-19 23:59:59&#39;)) 
(cost=0.40 rows=0) 
(actual time=0.049..0.049 rows=0 loops=1) 
-&gt; Index range scan on muge...</code></pre>
<p>analyze:</p>
<pre><code class="language-shell">-&gt; Index range scan on muge1_0 using ix_user_log_member_no_class_type_base_dt, 
with index condition: ((muge1_0.class_type = &#39;SEARCH&#39;) and (muge1_0.member_no = &#39;10044&#39;) and (muge1_0.base_dt between &#39;2024-11-26 00:00:00&#39; and &#39;2024-12-03 23:59:59&#39;))  
(cost=4.76 rows=10) 
(actual time=0.281..0.298 rows=10 loops=1)</code></pre>
<ul>
<li>Index range scan을 사용 (ix_user_log_member_no_class_type_base_dt 복합 인덱스)</li>
<li>예상 rows=10, 실제 rows=10</li>
<li>실행 시간: 0.281..0.298 밀리초</li>
<li>인덱스 조건으로 모든 WHERE절 조건을 사용</li>
</ul>
<h4 id="21-인덱스-관련">2.1 인덱스 관련</h4>
<p>테이블에 인덱스는 아래 3가지가 있었습니다.</p>
<ul>
<li>PRIMARY (base_Dt, member_no, game_id, class_type)</li>
<li>ix_member_no (member_no)</li>
<li>ix_member_no_class_type_base_dt (base_Dt, member_no, class_type)</li>
</ul>
<p>이번 이슈에서는 String 질의와 숫자형 질의 모두 ix_member_no는 사용하지 않았습니다. (base_dt가 선두인 나머지 두 인덱스가 날짜 범위 검색에 유리하기 때문에)</p>
<p>숫자형으로 질의한 경우 두 인덱스 모두 base_dt가 첫 컬럼이라 범위 검색에 유리하고, member_no도 둘 다 형변환이 발생해 효율적 활용이 어려우니 unique + 클러스터형 인덱스인 PRIMARY 를 채택한게 아닐까 싶습니다.
반대로 문자열로 질의한 경우는 쿼리의 조건절과 정확히 일치하는 인덱스 컬럼 구성을 가진 복합 인덱스를 채택했다고 생각합니다.
(RDB는 오랜만이라 틀린 부분이 있다면 지적부탁드립니다)</p>
<h4 id="3-재발-방지">3. 재발 방지</h4>
<p>재발 방지를 위해 hibernate 설정 추가로 애플리케이션 시작 시점에 Entity 클래스와 DB 스키마의 매핑을 검증하도록했습니다.
해당 옵션으로 설정하면, 타입 불일치가 발견되면 애플리케이션 시작이 실패하고 로그를 남기게됩니다.
허나 운영 환경에서는 비설정이 권장되기 때문에 Live 와 동일하게 설정되는 테스트 개발 환경 까지만 적용하여 검증하도록하였습니다.</p>
<pre><code class="language-yml">spring:
 jpa:
     properties:
         hibernate.hbm2ddl.auto: validate</code></pre>
<h3 id="todo">TODO</h3>
<ul>
<li>데이터베이스 컬럼 타입과 애플리케이션 코드 사이의 일관성을 검증하는 프로세스 구축</li>
<li>주요 쿼리들의 실행 계획을 정기적으로 리뷰하는 체계 도입</li>
<li>유사한 성능 이슈를 조기에 발견할 수 있는 모니터링 강화</li>
</ul>
<h3 id="ref">Ref.</h3>
<p><a href="https://docs.jboss.org/hibernate/orm/6.4/userguide/html_single/Hibernate_User_Guide.html#basic-mapping-convert">Hibernate ORM User Guide</a>
<a href="https://jakarta.ee/specifications/persistence/3.1/jakarta-persistence-spec-3.1.html#persistent-fields-and-properties">JPA Specification에서의 Type 매핑</a>
<a href="https://github.com/HomoEfficio/dev-tips/blob/master/hibernate.hbm2ddl.auto%20%EC%9C%84%ED%97%98%20%ED%97%B7%EC%A7%80.md">hibernate.hbm2ddl.auto 위험 헷지</a>
<a href="https://happy-coding-day.tistory.com/131">jpa field access?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Virtual Thread]]></title>
            <link>https://velog.io/@_koiil/Virtual-Thread</link>
            <guid>https://velog.io/@_koiil/Virtual-Thread</guid>
            <pubDate>Fri, 22 Nov 2024 04:19:25 GMT</pubDate>
            <description><![CDATA[<h3 id="virtual-thread">Virtual Thread</h3>
<p>Java의 동시성 프로그래밍은 오랫동안 OS 스레드를 직접 매핑한 Platform Thread를 기반으로 발전해왔습니다. 2023년 9월 19일, Java 21 LTS에서는 이러한 전통적인 스레드 모델과 함께 새로운 경량 스레드 모델인 <code>Virtual Thread</code>가 도입되었습니다.(<a href="https://openjdk.org/jeps/444">JEP 444: Virtual Threads</a>) 이는 최근 프로그래밍 언어들의 트렌드를 반영한 것으로, Go의 Goroutine, Kotlin의 Coroutine과 같은 경량 스레드 모델들이 높은 동시성 처리를 위한 해결책으로 주목받고 있는 흐름과 맥을 같이 합니다. 
<code>Lightweight Thread(경량 스레드)</code>는 실행 단위를 더 작은 단위로 나눠 Context switching 비용과 Blocking 타임을 낮추고, Kernel 레벨이 아닌 Runtime 레벨의 Task Scheduling 으로 효율적인 리소스 활용이 가능하다는 장점이 있어 클라우드 네이티브 환경에서의 대규모 동시성 처리에 적합한 해결책으로 떠오르고 있습니다.</p>
<h3 id="배경">배경</h3>
<p>전통적인 Java 웹 애플리케이션은 <code>Thread-per-Request</code> 모델을 기반으로 동작해왔습니다. 각 HTTP 요청마다 하나의 스레드가 할당되어 요청을 처리하는 방식은 직관적이고 이해하기 쉽다는 장점이 있습니다. 하지만 이 모델은 동시에 몇 가지 한계점을 가지고 있습니다.</p>
<ul>
<li><strong>제한된 처리량</strong>: OS 스레드는 생성과 유지에 상당한 시스템 리소스가 필요하기 때문에, 어플리케이션의 처리량이 Thread pool 크기에 직접적으로 제한되었습니다.</li>
<li><strong>비효율적인 리소스 활용</strong>: 특히 IO 작업이 많은 애플리케이션에서는 스레드들이 대부분의 시간을 blocking 상태로 보내게 되어, 리소스가 효율적으로 활용되지 못했습니다.</li>
</ul>
<p>이러한 문제들을 해결하기 위해 Spring WebFlux와 같은 Reactive Programming 모델이 등장했습니다. 이벤트 루프 기반의 non-blocking 모델은 적은 수의 스레드로 높은 처리량을 달성할 수 있었지만, 다음과 같은 새로운 문제들을 야기했습니다.</p>
<ul>
<li><strong>높은 학습 비용</strong>: 리액티브 프로그래밍은 기존의 명령형 프로그래밍과는 매우 다른 사고방식을 요구했습니다.</li>
<li><strong>라이브러리 호환성</strong>: 기존의 blocking IO 기반 라이브러리들을 모두 리액티브 방식으로 재작성해야 했습니다.(WebClient, R2DBC...)</li>
<li><strong>디버깅의 어려움</strong>: 요청이 여러 스레드를 넘나들며 처리되는 Reactive 방식은 Context 확인이 어려워 디버깅을 복잡하게 만들었습니다.</li>
</ul>
<p>이러한 배경에서 <code>Java Virtual Thread</code>는 다음과 같은 목표를 가지고 탄생했습니다.</p>
<ul>
<li><strong>높은 처리량</strong>: Reactive Programming의 장점인 높은 처리량을 달성</li>
<li><strong>쉬운 프로그래밍 모델</strong>: 전통적인 Thread 모델의 장점인 간단한 프로그래밍 모델을 유지</li>
<li><strong>기존 코드 호환성</strong>: 기존 Java 코드를 최소한의 수정으로 활용 가능</li>
<li><strong>디버깅 용이성</strong>: Thread Local, Exception, Profile 등 전통적인 자바 플랫폼의 방식을 그대로 사용 가능</li>
</ul>
<p>Virtual Thread는 JVM 내부에서 자체적으로 스케줄링되는 경량 실행 단위로, OS 스레드에 직접 매핑되는 대신 작업이 필요할 때만 Platform Thread(Carrier Thread)에 마운트되어 실행됩니다. 이를 통해 수십만 개의 동시 작업을 효율적으로 처리할 수 있게 되었고, 특히 IO 작업이 많은 워크로드에서 큰 성능 향상을 기대할 수 있게 되었습니다.</p>
<h3 id="platform-thread-vs-virtual-thread">Platform Thread vs Virtual Thread</h3>
<p><strong>Platform Thread (Java의 전통적인 Thread)</strong></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/a8249302-51df-4a45-9d22-40014cdaaff6/image.png" alt=""></p>
<ul>
<li>OS Thread를 1:1로 매핑한 JVM 수준의 추상화 구현체<ul>
<li>JVM이 Platform Thread를 생성할 때 JNI(Java Native Interface)를 통해 (Kernel)의 네이티브 스레드를 직접 할당받음</li>
<li>커널 스레드와 직접 매핑되어 있어 OS 스케줄러에 의해 CPU 코어에 직접 스케줄링됨</li>
</ul>
</li>
<li>생성 비용과 유지 비용이 높음<ul>
<li>각 스레드마다 고정된 스택 메모리(1MB)를 미리 할당</li>
<li>컨텍스트 스위칭 시 커널 모드 전환이 필요해 상대적으로 높은 비용 발생(1-10μs)</li>
<li>OS가 관리할 수 있는 스레드 수에 제한이 있어 동시 처리 가능한 요청 수가 제한됨</li>
</ul>
</li>
<li>Thread Pool을 통한 재사용이 필수적<ul>
<li>비싼 자원인 플랫폼 스레드를 생성/소멸 비용을 줄이기 위해 미리 생성된 스레드를 재사용</li>
<li>일반적으로 CPU 코어 수의 몇 배 정도로 풀 사이즈를 설정</li>
</ul>
</li>
</ul>
<p><strong>Virtual Thread</strong>
<img src="https://velog.velcdn.com/images/_koiil/post/d52d7ca3-f9b2-4be1-bf51-b24533166183/image.png" alt=""></p>
<ul>
<li>JVM 런타임에 의해 관리되는 경량 실행 단위<ul>
<li>OS 스레드에 직접 매핑되지 않고, 작업 실행이 필요할 때만 Platform Thread(Carrier Thread)에 마운트</li>
<li>JVM의 스케줄러가 Virtual Thread의 실행을 관리</li>
</ul>
</li>
<li>매우 적은 리소스로 생성 가능<ul>
<li>스레드당 메타데이터 크기가 약 200~300바이트로 매우 작음</li>
<li>스택 메모리를 heap에서 동적으로 할당하여 필요한 만큼만 사용</li>
<li>JVM 내부에서의 컨텍스트 스위칭으로 매우 빠른 전환 가능(나노초 단위)</li>
</ul>
</li>
<li>ForkJoinPool을 기반으로 한 효율적인 스케줄링<ul>
<li>기본적으로 CPU 코어 수만큼의 Carrier Thread가 Virtual Thread들을 실행</li>
<li>Work-Stealing 알고리즘을 통해 부하를 균등하게 분산</li>
<li>Blocking 작업 발생 시 자동으로 언마운트되어 다른 Virtual Thread가 실행됨</li>
</ul>
</li>
</ul>
<blockquote>
<table>
<thead>
<tr>
<th align="center">사용하는 자원</th>
<th align="center">Platform Thread</th>
<th align="center">Virtual Thread</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Metadata size</td>
<td align="center">약 2kb(OS별 차이 있음)</td>
<td align="center">200~300B</td>
</tr>
<tr>
<td align="center">Memory</td>
<td align="center">미리 할당된 Stack</td>
<td align="center">필요시 마다 Heap</td>
</tr>
<tr>
<td align="center">Context Switching Cost</td>
<td align="center">1-10us(커널 영역에서 발생)</td>
<td align="center">ns (or 1us 미만)</td>
</tr>
</tbody></table>
</blockquote>
<h3 id="quick-start">Quick Start</h3>
<p>Spring Boot 3.2 이상</p>
<pre><code class="language-yml"># application.yaml
spring:
  threads:
    virtual:
      enabled: true</code></pre>
<p>Spring Boot 3.2 미만</p>
<pre><code class="language-java">// Web Request 를 처리하는 Tomcat 이 Virtual Thread를 사용하도록 한다.
@Bean
public TomcatProtocolHandlerCustomizer&lt;?&gt; protocolHandlerVirtualThreadExecutorCustomizer() 
{
  return protocolHandler -&gt; {
    protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
  };
}

// Async Task에 Virtual Thread 사용
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
  return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}</code></pre>
<h3 id="사용-시-주의할-점">사용 시 주의할 점</h3>
<ul>
<li>ThreadLocal 사용 시 메모리 사용량 증가 (<a href="https://openjdk.org/jeps/425#Thread-local-variables">JEP 425: Thread-local variables</a>)<ul>
<li>Virtual Thread는 작업당 하나씩 생성을 권장하며, 각각 독립된 ThreadLocal 공간을 가짐</li>
<li>때문에 의도치않게 더 많은 메모리를 사용하는 원인이 될 수 있음</li>
<li>컨텍스트 전파가 필요한 경우 ScopedValue 또는 ThreadLocalAccessor 사용 권장</li>
</ul>
</li>
<li>synchronized 키워드 사용 시 성능 저하 (<a href="https://openjdk.org/jeps/425#Executing-virtual-threads">JEP 425: Pinning</a>)<ul>
<li>synchronized 블록 진입 시 Virtual Thread가 Carrier Thread에서 unmount 불가능한 상태가 됨 (Pinning)</li>
<li>ReentrantLock 등 java.util.concurrent 패키지의 락 구현체 사용 권장</li>
</ul>
</li>
<li>과도한 동시성으로 인한 리소스 부족 주의<ul>
<li>DB Connection Pool과 같은 제한된 자원에 대한 동시 접근이 증가하여 timeout 발생 가능 (SQLTransientConnectionException)</li>
<li>필요한 경우 Semaphore를 통한 동시성 제어 검토</li>
<li>HikariCP 등 Connection Pool 크기와 Virtual Thread 수 간의 적절한 조정 필요</li>
</ul>
</li>
<li>CPU bound 보다는 IO bound 워크로드에 적합<ul>
<li>CPU 코어에 스레드를 연결한 채 계산하는 CPU bound 워크로드는 Blocking 동안 대기하는 시간이 없기 때문에 이점이 없다.</li>
<li>오히려 Virtual Thread Scheduling 의 마운팅/언마운팅 오버헤드때문에 Platform Thread 가 더 효율적</li>
</ul>
</li>
<li>Structured Concurrency 활용 (<a href="https://openjdk.org/jeps/428">JEP 428</a>)<ul>
<li>기존의 CompletableFuture 체인은 작업 실패 시 리소스 누수나 에러 전파가 불명확할 수 있음</li>
<li>Virtual Thread 사용 시 StructuredTaskScope을 통해 작업 그룹의 생명주기를 명시적으로 관리하는 것을 권장</li>
<li>작업의 범위와 생명주기가 명확해져 디버깅과 유지보수가 용이</li>
</ul>
</li>
</ul>
<pre><code class="language-java">// 권장 X (작업 실패 시 다른 작업의 취소나 리소스 정리가 보장되지 않음)
CompletableFuture&lt;User&gt; user = CompletableFuture.supplyAsync(() -&gt; fetchUser());
CompletableFuture&lt;Order&gt; order = CompletableFuture.supplyAsync(() -&gt; fetchOrder());

// 권장
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future&lt;User&gt; user = scope.fork(() -&gt; fetchUser());
    Future&lt;Order&gt; order = scope.fork(() -&gt; fetchOrder());

    scope.join();          // 모든 작업 완료까지 대기
    scope.throwIfFailed(); // 작업 실패 시 모든 작업 취소 및 예외 전파

    // 모든 작업이 성공한 경우에만 실행
    processUserOrder(user.resultNow(), order.resultNow());
} // scope를 벗어나면 모든 자식 작업이 자동으로 정리됨</code></pre>
<blockquote>
<p><code>Scoped Value</code></p>
</blockquote>
<ul>
<li>ThreadLocal을 대체하기 위한 새로운 컨텍스트 전파 메커니즘 (<a href="https://openjdk.org/jeps/429">JEP 429</a>, <a href="https://openjdk.org/jeps/446">JEP 446</a>)<ul>
<li>Virtual Thread 환경에 최적화된 불변 컨텍스트 전달 방식</li>
<li>ThreadLocal과 달리 메모리 누수 위험이 없고 명시적인 스코프 관리</li>
<li>자식 Virtual Thread로의 자동 전파 지원<pre><code class="language-java">final ScopedValue&lt;User&gt; CURRENT_USER = ScopedValue.newInstance();
void processWithUser(User user) {
ScopedValue.where(CURRENT_USER, user)
    .run(() -&gt; {
        // 이 스코프 내의 모든 Virtual Thread에서 CURRENT_USER 접근 가능
        new VirtualThread(() -&gt; {
            User u = CURRENT_USER.get();
            processUserData(u);
        }).start();
    });
}</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="성능-테스트">성능 테스트</h3>
<p>8 Core 8 G Memory 인스턴스 2대로 성능 테스트를 진행한 결과입니다.
DB IO 보다는 외부 API 호출이 많기 때문에 DB 사용량은 두 케이스 모두 미미하고, Virtual Thread 가 아무리 빨라도 Response Time은 가장 느린 API 의 응답 시간과 동일했습니다.
동일하게 DB 에 대한 리소스는 고려해야할 정도는 아니었고, 외부 API 에 부담이 갈 정도의 성능을 요구하지는 않아 backpressure 처리는 톰캣 스레드만 플랫폼으로 연결하는 정도로 설정했습니다.</p>
<h4 id="virtual-thread-사용">Virtual Thread 사용</h4>
<ul>
<li>100 TPS</li>
<li>Avr Response time: 0.186 (sec)</li>
<li>CPU Utilization :  Server 35%, DB 1% 이하, Global Cache(Redis) 2%</li>
<li>요청 1회당 약 80개의 virtual thread 생성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_koiil/post/cb5824d8-5673-4d93-93b2-48341d6ca44c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/e6e8511d-a662-48b8-8975-358653883dbf/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/757d0d21-043f-4c3f-a037-1cdec364f991/image.png" alt=""></p>
<h4 id="platform-thread-사용">Platform Thread 사용</h4>
<ul>
<li>10 TPS</li>
<li>Max Response time: 30 (sec)</li>
<li>CPU Utilization :  Server 40% -&gt; 3% (응답 지연으로 인한 사용량 감소), DB 1% 이하, Global Cache(Redis) 2%</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_koiil/post/81429ff8-0798-4cce-8477-9c970899dbd4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/4bc70550-72ce-43d3-8d69-847f4dc0b972/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/ac4d2be8-e95a-4f8b-baa2-14afa89eba41/image.png" alt=""></p>
<h3 id="ref">Ref.</h3>
<ul>
<li><a href="https://openjdk.org/jeps/444">JEP 444</a>, <a href="https://openjdk.org/jeps/425">JEP 425</a></li>
<li><a href="https://www.youtube.com/watch?v=vQP6Rs-ywlQ&amp;list=PLZFZNBX75NZt_-NBd_oCkOQt2Hh5s_MfH&amp;index=6">JDK 21의 신기능 Virtual Thread 알아보기</a></li>
<li><a href="https://d2.naver.com/news/1203723">Virtual Thread의 기본 개념 이해하기</a></li>
<li><a href="https://quarkus.io/guides/virtual-threads">Virtual Thread support reference</a></li>
<li><a href="https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-BEC799E0-00E9-4386-B220-8839EA6B4F5C">Oracle docs - Virtual Threads</a></li>
<li><a href="https://findstar.pe.kr/2023/04/17/java-virtual-threads-1/">Virtual Thread란 무엇일까?</a></li>
<li><a href="https://tech.kakaopay.com/post/ro-spring-virtual-thread/">[Project Loom] Virtual Thread에 봄(Spring)은 왔는가</a></li>
<li><a href="https://techblog.woowahan.com/15398/">Java의 미래, Virtual Thread</a></li>
<li><a href="https://techblog.lycorp.co.jp/ko/about-java-virtual-thread-3">LY - Java 가상 스레드</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka: a Distributed Messaging System for Log Processing]]></title>
            <link>https://velog.io/@_koiil/Kafka-a-Distributed-Messaging-System-for-Log-Processing</link>
            <guid>https://velog.io/@_koiil/Kafka-a-Distributed-Messaging-System-for-Log-Processing</guid>
            <pubDate>Mon, 04 Nov 2024 10:58:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2017/09/Kafka.pdf">Kafka: a Distributed Messaging System for Log Processing</a></p>
</blockquote>
<h3 id="section-1">Section 1</h3>
<p>현대 인터넷 기업의 <code>로그 데이터</code>는 단순 분석용을 넘어 실시간 서비스의 핵심 요소가 되었다. </p>
<ul>
<li>로그인, 페이지뷰, 클릭 등 user action</li>
<li>call stack, latency, cpu 등 system metric</li>
</ul>
<p>위와 같은 로그 데이터는 검색, 광고, 추천, 보안, 뉴스피드 등의 실시간 서비스에 활용된다.</p>
<p>그러나 모든 real-time 로그를 전부 사용하는 것은 &quot;실제&quot;(유의미한) 데이터보다 엄청나게 많기 때문에 문제를 야기한다.
(ex. 광고, 추천, 검색 등은 세분화된 클릭률 계산을 위해 &quot;클릭&quot; 로그뿐 아닌 클릭되지 않은 수십개의 항목에 대한 로그도 생성한다.)</p>
<p>초기에는 이런 데이터 분석을 위해 프로덕션 서버에 로그 파일을 떨구고, 스크래핑 하는 방식에 의존했다. (Facebook의 Scribe, Yahoo의 Data Highway, Cloudera의 Flume...)</p>
<p>그러나 이런 시스템은 로그 데이터를 수집해 데이터 웨어하우스 또는 하둡에 로드하여 사용하도록 설계되어있다. (오프라인 분석용)</p>
<p>하지만 링크드인은 이러한 로그 수집 및 처리가 실시간으로 이루어져야한다고 생각했고, 기존 로그 애그리게이터와 메시징 시스템의 장점을 결합한 새로운 로그 처리용 메시징 시스템인 Kafka를 구축햇다.</p>
<h3 id="section-2-traditional-messaging-systems-and-log-aggregators">Section 2 traditional messaging systems and log aggregators</h3>
<ul>
<li>기존 메세지 시스템은 다양한 전송 보장에 중점을 두는 등 로그 프로세싱에는 오버스펙인 경우가 많았다. <ul>
<li>ex. IBM Websphere MQ wsAtomicTransaction, JMS..</li>
<li>위같은 불필요 기능들때문에 api 와 구현의 복잡도가 올라갔다.</li>
</ul>
</li>
<li>대부분의 서비스에서 처리량은 주요 설계 제약 조건이 아니었다.<ul>
<li>JMS 는 명시적으로는 하나의 요청을 통한 멀티 메세지 컨슈밍을 지원하지 않음</li>
</ul>
</li>
<li>분산 지원이 취약하다.(여러 시스템에 파티션을 분산해 저장하는 방법이 없음)</li>
<li>거의 즉각적 소비를 가정한다<ul>
<li>데이터 웨어하우징같이 배치로 큰 소비를 하는 케이스는 메세지가 누적되어 성능이 저하됨</li>
</ul>
</li>
</ul>
<p>기존의 메세지 시스템들은 아래와 같이 동작</p>
<ol>
<li>클라이언트가 로그데이터(이벤트)를 시스템으로 전송</li>
<li>롤아웃된 파일을 주기적으로 HDFS (혹은 NFS) 장치에 덤프</li>
</ol>
<p>위같은 방식의 문제:</p>
<ul>
<li>대부분 오프라인에서 로그데이터 소비를 가정하고 구축되어 실시간과는 맞지 않다</li>
<li>세부 정보(롤아웃된 파일 등)을 불필요하게 소비자에게 노출시킨다</li>
<li>대부분 push 방식 사용하나 링크드인은 pull 방식이 적합하다고 생각했다<ul>
<li>소비자의 처리량을 초과하여 푸시되는 경우를 예방 가능</li>
<li>소비자가 재처리하기 용이</li>
</ul>
</li>
</ul>
<p>이후 야후 리서치에서 HedWig라는 새로운 분산형 펍/섭 시스템을 개발했고, 확장성과 가용성이 뛰어나며 강력한 내구성을 한다. 
그러나 주로 데이터 저장소의 커밋 로그를 저장하기 위함이다.</p>
<h3 id="section-3-architecture-and-key-design-principles-of-kafka">Section 3 architecture and key design principles of Kafka</h3>
<p>위같은 기존 시스템의 한계로 새로운 메시징기반 로그 수집기인 카프카 개발하게 되었다.</p>
<p>기본적으로 메시징은 개념적으로 단순하며, 이를 반영하기 위해 Kafka API도 단순하게 구성되었다.</p>
<ul>
<li>특정 유형의 메시지 스트림은 토픽으로 정의</li>
<li>프로듀서는 토픽에 메세지를 발행</li>
<li>발행된 메세지는 브로커라고 하는 서버 집합에 저장</li>
<li>소비자는 브로커에서 하나 이상의 토픽을 구독하고, 메세지를 소비</li>
</ul>
<pre><code class="language-java">// Sample producer code:

 producer = new Producer(…);
 // 메세지는 바이트 단위의 페이로드만 포함
 message = new Message(“test message str”.getBytes());
 // MessageSet 을 통해 한번의 요청으로 효율적으로 발행 가능
 set = new MessageSet(message)

// Sample consumer code:

 // 토픽에 대한 메세지 스트림 생성
 streams[] = Consumer.createMessageStreams(“topic1”, 1)
 // iterator 을 통해 메세지 처리
 for (message : streams[0]) {
     bytes = message.payload();
 // do something with the bytes
 }
 // 소비할 메세지가 더이상 없는 경우 새 메세지가 토픽에 게시될 때까지 차단</code></pre>
<p>Kafka는 본질적으로 분산되어 있기 때문에, Kafka 클러스터는 여러 개의 브로커로 구성되고 토픽은 여러 파티션으로 나뉘며 각 브로커는 하나 이상의 파티션 저장된다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/bda7f025-a26e-4f42-ab55-ed3aeaac5d30/image.png" alt=""></p>
<h4 id="31-efficiency-on-a-single-partition">3.1 Efficiency on a Single Partition</h4>
<p>브로커에서 단일 파티션의 레이아웃과 파티션에 효율적으로 액세스하기 위한 디자인을 채택했다.
<code>Simple Storage</code></p>
<ul>
<li>토픽의 각 파티션은 논리적 로그에 해당한다.</li>
<li>물리적으로 로그는 거의 동일한 크기(예: 1GB)의 세그먼트 파일 집합으로 구현된다.</li>
<li>프로듀서가 파티션에 메시지를 발행할 때마다 브로커는 마지막 세그먼트 파일에 메시지를 추가하기만 하면 된다. </li>
<li>성능을 위해 min size 나 batch time 후에만 세그먼트 파일을 디스크에 플러시한다. </li>
<li>메시지는 플러시된 후에만 소비자에게 노출된다.</li>
</ul>
<p>Kafka는 메시지 식별을 위해 로그의 논리적 오프셋을 사용하여 복잡한 인덱싱 구조를 제거했다.</p>
<p>카프카의 메세지에는 명시적인 id 가 없는 대신 오프셋 주소가 지정된다.
때문에 메세지 id 를 실제 위치에 매핑하는 탐색-intensive 한 랜덤 엑세스 인덱스 구조를 유지하는 오버헤드를 피할 수 있다.</p>
<p>메세지 Id 는 sequencial하게 증가하지만 하지만 연속적이지는 않다.(다음 메세지의 ID = 현재 메세지 ID +  현재 메세지 Length)</p>
<p>소비자가 특정 메세지 오프셋을 처리하면 해당 파티션에 해당 오프셋 이전의 모든 메세지를 수신했음을 의미.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/de79b998-21cf-4752-9e44-538ee24f968b/image.png" alt=""></p>
<p><strong>효율적인 데이터 전송 전략</strong></p>
<ul>
<li>일괄 처리<ul>
<li>프로듀서의 단일 요청으로 다수 메시지(message set) 전송 가능</li>
<li>소비자의 한 번의 pull 요청으로 수백 KB 메시지 검색할 수 있다.</li>
</ul>
</li>
<li>캐시 최적화: Kafka 레이어 메모리에 캐싱 대신 파일 시스템 페이지 캐시 활용<ul>
<li>이중 버퍼링 방지</li>
<li>브로커 재시작되어도 캐시 유지</li>
<li>프로세스에서 전혀 메시지를 캐싱하지 않기 때문에 낮은 GC 오버헤드</li>
<li>컨슈머가 프로듀서보다 조금 늦게 세그먼트에 접근함으로써 OS 캐싱 휴리스틱이 효과적으로 작동</li>
</ul>
</li>
<li>네트워크 최적화: sendfile API 활용<ul>
<li>하나의 메시지가 여러 번 소비되는 다중 구독자 시스템</li>
<li>일반적인 파일-소켓 전송은 4번의 데이터 복사와 2번의 시스템 콜 필요</li>
<li>sendfile API는 2번의 데이터 복사와 1회의 시스템 콜 필요 (파일 채널에서 소켓 채널로 바이트를 직접 전송, 일반의 1, 4번만 실행)</li>
<li>테라바이트 규모에서도 데이터 크기에 비례하는 일관된 성능 유지</li>
</ul>
</li>
</ul>
<p>로컬 파일에서 원격 소켓으로 바이트를 전송하는 일반적인 방법(4번의 데이터 복사와 2번의 시스템 콜)</p>
<ul>
<li>(1) 저장 매체에서 OS 페이지 캐시로 데이터를 읽습니다.</li>
<li>(2) 페이지 캐시의 데이터를 애플리케이션 버퍼로 복사합니다.</li>
<li>(3) 애플리케이션 버퍼를 다른 커널 버퍼로 복사합니다.</li>
<li>(4) 커널 버퍼를 소켓으로 전송합니다.</li>
</ul>
<p><strong>브로커의 무상태성</strong> </p>
<ul>
<li>각 컨슈머가 얼마나 소비했는지에 대한 정보가 브로커가 아닌 컨슈머 자체에 의해 유지<ul>
<li>브로커의 복잡성과 오버헤드 감소하나 어디까지 소비되었는지 알 수 없어 메세지 삭제가 까다로워짐</li>
<li>메시지 삭제 정책: 시간 기반 SLA 적용 (일반적으로 7일 뒤 삭제)</li>
</ul>
</li>
<li>소비자가 의도적으로 이전 오프셋으로 돌아가 데이터를 재소비할 수 있다<ul>
<li>일반적인 Queue와의 차이</li>
<li>컨슈머 로직 수정 후 메시지 재처리 가능(하둡 등 ETL 데이터 로드에 중요)</li>
<li>주기적으로만 영구 저장소(ES 등)에 데이터를 기록하는 경우 장애 발생 시 데이터유실 대신 마지막 오프셋부터 재처리 가능</li>
</ul>
</li>
</ul>
<h4 id="32-distributed-coordination">3.2 Distributed Coordination</h4>
<p>분산 환경에서 프로듀서와 컨슈머의 동작</p>
<p>각 프로듀서는 랜덤한 파티션 혹은 파티션 키와 함수로 지정된 파티션에 메세지를 발행한다.(파티셔너)</p>
<p>카프카에는 컨슈머그룹이라는 개념이 있다.</p>
<p>컨슈머 그룹은 구독한 토픽을 공동으로 소비하고, 각 메세지는 그룹 내 하나의 컨슈머에만 전달된다.
컨슈머 그룹들은 각각 독립적으로 토픽에 대한 메세지를 소비한다. (그룹별 오프셋 관리)
그룹 내의 컨슈머들은 다른 프로세스 혹은 장비에 있을 수 있다.</p>
<p>카프카의 목표는 브로커에 저장된 메세지를 &quot;오버헤드 없이&quot; &quot;균등하게&quot; 나누는 것이다.</p>
<ol>
<li>토픽의 파티션을 병렬 처리의 가장 작은 단위로 지정</li>
</ol>
<p>즉 파티션과 컨슈머를 1:1로 할당하므로써 여러 소비자가 하나의 파티션에 대해 누가 어떤 메세지를 소비할 지 조정하기 위한 잠금 및 상태 유지 관리 오버헤드를 없앴다.(하지만 컨슈머 그룹은 부하 재조정을 위해 간혹 리밸런싱이 필요하다.)
토픽의 오버 파티셔닝을 통해 소비자보다 파티션이 많게 하여 로드밸런싱을 달성했다.</p>
<ol start="2">
<li>중앙 마스터 노드 대신 컨슈머들이 분산된 방식으로 협력하도록 함</li>
</ol>
<p>마스터 노드를 두게 되면 마스터 장애에 대한 처리로 시스템이 복잡해질 수 있다.
때문에 고가용성 합의 서비스인 Zookeeper 을 사용</p>
<ul>
<li>주키퍼는 API 같은 간단한 파일시스템을 가지고 있다</li>
<li>path 등록/값 설정/읽기/삭제/하위 path 조회 등</li>
<li>path에 watcher 을 등록해 변경 추적, 인스턴스 path 생성(클라이언트 삭제 시 자동 제거), replication 등의 기능 지원</li>
</ul>
<p>카프카는 다음과 같은 작업에 주키퍼를 사용한다</p>
<ul>
<li>브로커와 컨슈머 추가 및 제거 감지</li>
<li>위와 같은 변경 발생 시 컨슈머 리밸런싱 프로세스 트리거</li>
<li>컨슈머 상태 체크와 각 파티션의 오프셋 추적 등</li>
</ul>
<ol>
<li>브로커/컨슈머 시작(등록)하면 카프카 레지스트리에 정보 저장</li>
</ol>
<ul>
<li>Brocker registry: host name, port, 브로커에 저장된 topic&amp;partition </li>
<li>Consumer registry: Consumer Group 정보, 해당 그룹이 구독하는 topic 목록</li>
</ul>
<ol start="2">
<li>각 컨슈머그룹은 Zookeeper 소유권 레지스트리 &amp; 오프셋 레지스트리에 연결</li>
</ol>
<ul>
<li>소유권 레지스트리에는 소유권을 가진 컨슈머 ID 경로에 구독하는 파티션이 있음.</li>
<li>오프셋 레지스트리는 각 파티션에 대해 파티션에서 마지막으로 소비된 메세지의 오프셋을 저장</li>
</ul>
<p>브로커, 컨슈머, 소유권 레지스트리는 임시 경로이고, 오프셋 레지스트리는 영구 경로이다.
브로커에 오류가 발생하면 브로커에 있는 모든 파티션이 브로커 레지스트리에서 제거된다.
컨슈머에 오류가 발생하면 컨슈머 레지스트리에 등록된 항목을 제거하고 소유하는 모든 파티션을 소유권 레지스트리에서 제거한다.</p>
<p>컨슈머가 시작되거나 브로커/컨슈머의 변경을 감지하면 컨슈머는 리밸런싱 프로세스를 시작해 담당 파티션을 재분배한다.</p>
<blockquote>
<p>리밸런싱 알고리즘
<img src="https://velog.velcdn.com/images/_koiil/post/a78b27e2-f909-4ff3-ae0d-0b9e6b7ed2b6/image.png" alt=""></p>
</blockquote>
<ol>
<li>Zookeeper 에서 브로커와 소비자 레지스트리를 읽어 각 구독 토픽(T)에 대해 <strong>사용 가능한 파티션 집합(PT)</strong>과 <strong>T를 구독하는 컨슈머 집합(CT)</strong>을 계산</li>
<li>PT를 |CT| 청크로 범위 분할하고 각각 소유할 청크 하나를 선택</li>
<li>컨슈머가 선택한 각 파티션에 대해 소유권 레지스트리에 해당 파티션의 새 소유자로 기록</li>
<li>컨슈머는 오프셋 레지스트리에 지정된 오프셋을 읽어 해당 지점부터 소비 재개</li>
<li>파티션에서 메세지를 소비하면 오프셋 레지스트리에 가장 최근에 소비된 오프셋을 주기적으로 기록</li>
</ol>
<p>컨슈머 그룹 내의 컨슈머들은 변경에 대한 알림을 받는 시점이 조금씩 다를 수 있다.
때문에 컨슈머1이 컨슈머2가 아직 소유하고있는 파티션을 소유하려할 수 있다.
이 경우 컨슈머1은 현재 소유중인 파티션을 모두 해제하고 잠시 대기했다가 리밸런싱 프로세스를 다시 시도
실제로 리밸런싱 프로세스는 몇 번의 재시도 후에 안정화된다.</p>
<p>새 컨슈머그룹이 생성되면 오프셋 레지스트리에는 사용할 수 있는 오프셋이 없다.
이 경우 컨슈머는 브로커에서 제공하는 api 를 통해 구독된 각 파티션에서 사용가능한 min/max 오프셋으로 시작한다.</p>
<h4 id="33-delivery-guarantees">3.3 Delivery Guarantees</h4>
<ul>
<li>일반적으로 카프카는 at least once 를 보장하나, exactly once 보장을 위해서는 2 Phase Commit 이 필요하다<ul>
<li>대부분의 메세지는 각 컨슈머그룹에 exactly once 로 전달되나, 컨슈머 리밸런싱 과정에서 오프셋이 커밋되기 전에 종료되면 중복 처리 될 수 있다.</li>
<li>중복 방지가 중요한 경우 컨슈머에 메세지키 혹은 오프셋을 통한 중복 처리 방지 로직을 추가해야한다. (2phase commit 보다 효율적이다.)</li>
</ul>
</li>
<li>단일 파티션의 메세지는 순서를 보장하나 다른 파티션간의 순서는 보장하지 않는다.</li>
<li>로그 손상을 방지하기 위해 카프카는 로그에 각 메세지에 대한 CRC(cyclic redundancy check)를 저장한다.<ul>
<li>브로커에  I/O 에러가 발생하면 카프카는 복구 프로세스를 실행해 일관되지 않은 CRC를 가진 메세지를 제거한다.</li>
<li>메세지에 대한 CRC를 체크하면 메세지의 생성/소비 이후에도 네트워크 오류를 확인할 수 있다.</li>
</ul>
</li>
<li>브로커가 다운되면 소비되지 않은 메세지는 사용할 수 없게된다. 브로커의 저장 시스템이 영구적으로 손상되면 소비되지 않은 메세지는 영구 손실된다.<ul>
<li>때문에 카프카의 replication 을 사용해야한다.</li>
</ul>
</li>
</ul>
<h3 id="section-4-deployment-of-kafka-at-linkedin">Section 4 deployment of Kafka at LinkedIn</h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/1b68863c-11e6-4eaf-856a-23607152d2b6/image.png" alt=""></p>
<p>링크드인은 데이터 센터당 하나의 Kafka 클러스터가 있다.</p>
<ul>
<li>프론트는 다양한 종류의 로그 데이터를 생성하고, 이를 배치와 LB를 통해 브로커 집합에 고르게 전송한다.</li>
<li>Kafka의 온라인 컨슈머는 동일한 데이터 센터 내의 서비스에서 실행된다.</li>
<li>또한 오프라인 분석을 위해 별도의 데이터 센터에 Kafka 클러스터를 배포한다.<ul>
<li>이 클러스터는 하둡 클러스터, 기타 데이터 웨어하우스 인프라에 지리적으로 가까이 있다.</li>
<li>실시간 데이터센터의 Kafka 인스턴스에서 데이터를 가져오기 위해 내장된 컨슈머 그룹을 실행한다.</li>
<li>하둡 혹은 DW에서 복제 Kafka 클러스터에서 데이터를 가져오기 위한 로드를 실행한다.</li>
<li>해당 데이터를 분석해 다양한 리포팅, 프로토타이핑 작업을 실행한다.</li>
</ul>
</li>
<li>전체 파이프라인에서 데이터 손실이 없는지 확인하기 위한 감사(auditing) 시스템도 포함되어 있다.<ul>
<li>각 메시지는 생성될 때 타임스탬프와 서버 이름을 포함한다.</li>
<li>각 프로듀서를 측정해 fixed time window 내에서 해당 프로튜서가 각 토픽에 게시한 메시지 수를 기록하는 모니터링 이벤트를 주기적으로 생성한다.</li>
<li>컨슈머는 특정 토픽에서 수신한 메시지 수를 계산하고, 모니터링 이벤트와 비교하여 데이터의 정확성을 확인한다.</li>
</ul>
</li>
</ul>
<h3 id="section-5-performance-results-of-kafka">Section 5 performance results of Kafka</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[Apache Kafka - producer]]></title>
            <link>https://velog.io/@_koiil/kafka-producer</link>
            <guid>https://velog.io/@_koiil/kafka-producer</guid>
            <pubDate>Tue, 08 Oct 2024 11:00:24 GMT</pubDate>
            <description><![CDATA[<h3 id="프로듀서-전송-과정">프로듀서 전송 과정</h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/aa4aa264-56c6-4c7a-b1e5-ea42149284e1/image.png" alt=""></p>
<ul>
<li>ProducerRecord 객체 생성</li>
<li>key &amp; value 바이트 배열로 직렬화</li>
<li>파티셔너로 전송</li>
<li>레코드 배치에 데이터 추가</li>
<li>별도의 스레드가 레코드 배치를 브로커에 전송</li>
<li>브로커는 저장에 성공하면 RecordMetadata 객체 리턴</li>
<li>RecordMetadata =&gt; 토픽 + 파티션 + 파티션의 레코드 오프셋 </li>
<li>실패하면 에러 리턴</li>
<li>프로듀서는 에러를 받으면 사용자에게 보내지 않고 재전송 수행</li>
</ul>
<blockquote>
<p>Ref. <a href="https://d2.naver.com/helloworld/6560422">KafkaProducer Client Internals</a></p>
</blockquote>
<h3 id="프로듀서-생성하기">프로듀서 생성하기</h3>
<p>필수 속성값</p>
<ul>
<li><code>bootstrap.servers</code><ul>
<li>브로커의 host:port 목록 (ex) 10.x.x.x:9092,10.x.x.x:9092,...</li>
<li>모든 브로커를 포함하지 않아도 상관없다.</li>
<li>두대 이상 지정 권장 (만약 둘 중에 하나가 정지하면 다른 하나로 연결하기 위함)</li>
</ul>
</li>
<li><code>key.serializer</code>, <code>value.serializer</code><ul>
<li>위의 그림에서 Serializer</li>
<li>ByteArraySerializer, StringSerializer, IntegerSerializer 등이 있다.</li>
<li>String 보낼거면 StringSerializer 사용</li>
<li>key 지정하지 않아도 설정 필요하다. 이런 경우 보통은 VoidSerializer 사용</li>
</ul>
</li>
</ul>
<p>예제 </p>
<pre><code class="language-java">Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:9092&quot;);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
KafkaProducer&lt;String, String&gt; producer = new KafkaProducer&lt;&gt;(properties);</code></pre>
<h3 id="메시지-전달하기">메시지 전달하기</h3>
<pre><code class="language-java">//  정한 타입이 일치해야함
ProducerRecord&lt;String, String&gt; record = new ProducerRecord&lt;&gt;(&quot;test1&quot;, &quot;test2&quot;, &quot;test3&quot;);
try {
// send() 메소드는 RecordMetadata를 포함한 자바 Future 객체를 리턴하지만, 여기서는 무시하기 때문에 전송 성공 여부를 알 수 없다.
    producer.send(record);
} catch(Exception e) {
/** 
 전송할 때 혹은 브로커 자체 에러를 무시하더라도 프로듀서가 메시지를 보내기 전 에러가 발생할 경우 예외 발생 가능.
 SerializationException -&gt; 직렬화 실패 경우
 TimeoutException -&gt; 버퍼가 가득 찬 경우
 InterruptException -&gt; 스레드에 인터럽트가 걸리는 경우
**/
    e.printStackTrace();
}</code></pre>
<p><strong>동기적 전송</strong>
작업이 몰리면 브로커가 쓰기 요청에 응답하기까지 최소 2ms 최대 몇 초가 지연 될 수 있다.
전송 요청하는 스레드는 위 시간동안 아무 작업도 할 수 없다.
(동기적 전송은 잘 안쓴다.)</p>
<pre><code class="language-java">ProducerRecord&lt;String, String&gt; record = new ProducerRecord&lt;&gt;(&quot;test1&quot;, &quot;test2&quot;, &quot;test3&quot;);
try {
// 카프카로 응답이 올 때 까지 대기하기 위해 .get() 사용
    producer.send(record).get();
} catch (Exception e) {
    e.printStackTrace();
}</code></pre>
<p><strong>비동기적 전송</strong>
카프카는 레코드를 쓴 뒤 해당 레코드의 토픽, 파티션 오프셋을 리턴하는데, 대부분의 애플리케이션은 이런 메타데이터가 필요가 없다.
메시지 전송이 실패했을 경우에는 해당 내용을 알아야 하는데, 이때는 콜백을 사용한다.</p>
<pre><code class="language-java">// org.apache.kafka.clients.producer.Callback 인터페이스를 구현하는 클래스 필요.
// 해당 인터페이스에는 onCompletion() 단 하나의 메서드만 정의되어 있다.
private class TestProducerCallback implements Callback {
    @Override
    public void onCompletion(RecordMetadata recordMetadata, Exception e) {
        if (e != null) {
// 만약 에러를 리턴하면 onCompletion() 메서드가 null이 아닌 Exception 객체를 받게된다.
            e.printStackTrace();
        }
    }
}

ProducerRecord&lt;String, String&gt; record = new ProducerRecord&lt;&gt;(&quot;test1&quot;, &quot;test2&quot;, &quot;test3&quot;);
/// Callback 객체 매개변수로 전달
producer.send(record, new TestProducerCallback());</code></pre>
<p><strong>KafkaProducer 에러 종류</strong></p>
<ul>
<li>재시도 가능한 에러 (메시지를 다시 전송 가능한 에러)<ul>
<li>연결 에러</li>
<li>네트워크 타임아웃</li>
<li>리더 선출</li>
<li>파티션 리밸런싱 에러</li>
</ul>
</li>
<li>재시도 불가능 에러<ul>
<li>직렬화 실패</li>
<li>producer 버퍼 풀</li>
<li>메시지 크기가 너무 클 경우</li>
</ul>
</li>
</ul>
<h3 id="일반-옵션">일반 옵션</h3>
<blockquote>
<p>아래의 설정들은 프로듀서에서만 설정이 가능하다. (브로커에서 설정 불가능)
하기 옵션을 제외한 나머지 옵션은 기본으로 두고 쓰는걸 권장한다.
kafka 2.22 기준</p>
</blockquote>
<pre><code class="language-java">// 옵션 적용 방법
props.put(&quot;옵션 이름&quot;, &quot;옵션 값&quot;);
props.put(ProducerConfig.옵션 이름, &quot;옵션 값&quot;);</code></pre>
<h4 id="clientid">client.id</h4>
<ul>
<li>프로듀서 구분하기 위한 논리적 식별자</li>
<li>설정하면 프로듀서 모니터링시 구별하기 쉽다.</li>
</ul>
<h4 id="acks">acks</h4>
<ul>
<li>프로듀서가 쓰기 작업이 성공했다고 판별하기 위한 값</li>
</ul>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center">acks = 0</th>
<th align="center">acks = 1</th>
<th align="center">acks = all</th>
</tr>
</thead>
<tbody><tr>
<td align="center">성공 기준</td>
<td align="center">전송 후(브로커의 응답을 기다리지 않음)</td>
<td align="center">리더 브로커가 받으면</td>
<td align="center">모든 복제가 완료</td>
</tr>
<tr>
<td align="center">특징</td>
<td align="center">처리량이 매우 중요하고, 유실이 되어도 상관없으면 설정</td>
<td align="center">리더가 받고 복제 직전에 죽어버리면 메시지가 유실된다.리더가 그 전에 죽으면 재전송.</td>
<td align="center">최소 2개 이상의 브로커가 해당 메시지를 가지고 있고, 크래시가 나도 유실되지 않기 때문에 가장 안전하나 응답까지의 시간이 다른 설정보다 오래 걸린다</td>
</tr>
</tbody></table>
<h4 id="buffermemory">buffer.memory</h4>
<ul>
<li>프로듀서가 메시지를 전송하기 전에 메시지를 대기시키는 버퍼의 크기</li>
<li>버퍼가 가득 차면 추가로 호출되는 send()는 max.block.ms 동안 블록되어  버퍼 메모리에 공간이 생기기를 기다리게 되는데, 해당 시간 동안 대기하고 공간 확보가 안되면 예외 발생</li>
</ul>
<h4 id="compressiontype">compression.type</h4>
<ul>
<li>기본적으로 압축 비활성</li>
<li>snappy, gzip, lz4, zstd 가 있으며, 사용 시 메시지를 압축하여 브로커로 전송<ul>
<li>Snappy 압축 알고리즘이 CPU 부하도 적고 압축 성능도 적당히 좋다.</li>
<li>만약 높은 압축률이 필요하다면 Gzip 사용</li>
</ul>
</li>
<li>압축 사용하면 CPU 사용률이 올라감</li>
</ul>
<h4 id="batchsize">batch.size</h4>
<ul>
<li>같은 파티션에 다수의 레코드가 전송되면 프로듀서는 이를 배치로 모아서 한꺼번에 전송한다.
각 배치에 사용될 메모리의 양을 결정(바이트 단위임)</li>
<li>배치가 가득 차면 해당 배치의 모든 메시지가 전송된다.<ul>
<li>배치가 가득 찰 때까지 기다리는 것은 아니고, 아래 <code>linger.ms</code> 에 설정된 제한 시간이 되면 배치를 전송한다.</li>
</ul>
</li>
<li>너무 작게 설정하면 지나치게 자주 메시지를 전송하기 때문에 약간의 오버헤드 발생 가능</li>
</ul>
<h4 id="maxinflightrequestsperconnection">max.in.flight.requests.per.connection</h4>
<ul>
<li>프로듀서가 서버로부터 응답을 받지 못한 상태에서 전송할 수 있는 최대 메시지의 수</li>
<li>값을 올리면 메모리 사용량이 늘지만 처리량도 증가함</li>
<li>문서에 따르면 2일 때 처리량이 최대를 기록하지만, 기본값인 5를 사용해도 비슷한 성능을 보여준다고 한다.</li>
</ul>
<h4 id="maxrequestsize">max.request.size</h4>
<ul>
<li>프로듀서가 전송하는 쓰기 요청이 크기를 결정</li>
<li>메시지 최대 크기를 제한하기도 하지만, 한 번의 요청에 보낼 수 있는 메시지 최대 개수 역시 제한</li>
<li>브로커의 <code>message.max.bytes</code> 설정과 동일하게 설정하는게 좋음.</li>
</ul>
<h4 id="receivebufferbytes-sendbufferbytes">receive.buffer.bytes, send.buffer.bytes</h4>
<ul>
<li>데이터를 읽거나 쓸 때 소켓이 사용하는 TCP 송수신 버퍼의 크기를 결정</li>
<li>-1일 경우 운영체제의 기본값 사용</li>
<li>프로듀서나 컨슈머가 다른 데이터센터에 위치한 브로커와 통신할 경우 값을 올려주는게 좋다.</li>
</ul>
<h4 id="enableidempotence">enable.idempotence</h4>
<ul>
<li>정확히 한 번 옵션 사용(카프카 버전 0.11부터 가능)</li>
<li>해당 옵션을 활성화 하면 메시지에 번호를 붙여서 발행한다.</li>
</ul>
<h3 id="메시지-전달-시간-관련-옵션">메시지 전달 시간 관련 옵션</h3>
<p>ProducerRecord를 보낼 때 걸리는 시간이 두 구간으로 나뉘어 있다.</p>
<ul>
<li><code>send()</code>에 대한 비동기 호출이 이뤄진 시각부터 결과를 리턴할 때까지 걸리는 시간<ul>
<li>이 시간 동안 <code>send()</code>를 호출한 스레드는 블록</li>
</ul>
</li>
<li><code>send()에</code> 대한 비동기 호출이 성공적으로 리턴한 시각부터 콜백이 호출될 때까지 걸리는 시간<ul>
<li>ProducerRecord가 전송을 위해 배치에 추가된 시점부터 전송을 위해 할당된 시간이 소진될 때까지의 시간과 동일</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_koiil/post/ce9715e1-a3eb-40e6-90a1-0c5a8e0f35c5/image.png" alt=""></p>
<h4 id="maxblockms">max.block.ms</h4>
<ul>
<li>프로듀서가 아래의 경우 얼마나 오랫동안 블록되는지 결정<ul>
<li><code>send()</code>를 호출했을 때</li>
<li><code>partitionsFor</code>를 호출해서 명시적으로 메타데이터를 요청했을 때</li>
</ul>
</li>
<li>프로듀서의 전송 버퍼가 가득 차거나 메타데이터가 아직 사용 가능하지 않을 때 블록된다. <ul>
<li>이 상태에서 <code>max.block.ms</code>만큼 시간이 흐르면 예외가 발생</li>
</ul>
</li>
</ul>
<h4 id="deliverytimeoutms">delivery.timeout.ms</h4>
<ul>
<li>레코드 전송 준비가 완료된 시점(<code>send()</code>가 정상적으로 리턴되고 레코드가 배치에 저장된 시점)부터 브로커의 응답을 받거나 아니면 전송을 포기하게 되는 시점까지의 제한시간을 결정</li>
<li><code>linger.ms, request.timeout.ms</code>보다 커야함.</li>
<li>만약 프로듀서가 재시도를 하는 도중에 <code>delivery.timeout.ms</code>가 넘어가면, 마지막으로 재시도 하기 전에 브로커가 리턴한 예외와 함께 콜백이 호출</li>
<li>레코드 배치가 전송을 기다리는 와중에 <code>delivery.timeout.ms</code>가 넘어가버리면 타임아웃 예외와 함께 콜백이 호출</li>
</ul>
<h4 id="requesttimeoutms">request.timeout.ms</h4>
<ul>
<li>프로듀서가 데이터를 전송할 때 서버로부터 응답을 받기 위해 얼마나 기다릴지 결정.</li>
</ul>
<h4 id="retries-retrybackoffms">retries, retry.backoff.ms</h4>
<ul>
<li>프로듀서가 서버로부터  에러 메시지를 받았을 때 retries는 재전송하는 횟수를 결정.</li>
<li>기본적으로 프로듀서는 각 재시도 사이에 100ms 대기한다. 이 값은 <code>retry.backoff.ms</code> 설정을 통해 조정 가능</li>
<li>임의로 브로커 죽이고 다시 돌아오는데 걸리는 시간을 측정하고 <code>delivery.timeout.ms</code>로 잡아주는걸 권장.</li>
</ul>
<h4 id="lingerms">linger.ms</h4>
<ul>
<li>현재 배치를 전송하기 전까지 대기하는 시간을 결정</li>
<li>producer는 현재 배치가 가득 차거나 <code>linger.ms</code>에 설정된 제한 시간이 되었을 때 메시지 배치를 전송.</li>
<li>기본은 메시지 전송에 사용 가능한 스레드가 있으면 바로 전송함</li>
<li>해당 값을 증가 시키면, 지연이 조금 증가하는 대신 처리율을 크게 증가시킬 수 있다.</li>
<li>단위 메시지당 추가적으로 드는 시간은 매우 작지만 압축이 설정되어 있거나 하면 효율적이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Apache Kafka - 개요와 구성 요소]]></title>
            <link>https://velog.io/@_koiil/Apache-Kafka</link>
            <guid>https://velog.io/@_koiil/Apache-Kafka</guid>
            <pubDate>Fri, 29 Mar 2024 04:11:32 GMT</pubDate>
            <description><![CDATA[<h3 id="apache-kafka-란">Apache Kafka 란?</h3>
<blockquote>
<p><strong>&quot;분산 스트리밍 플랫폼&quot;</strong>
대량의 데이터를 처리, <strong>실시간</strong>으로 전송한다.</p>
</blockquote>
<h4 id="등장-배경">등장 배경</h4>
<ul>
<li>문제: 링크드인에서 수많은 데이터를 실시간으로 전송하고 수신하는 과정에서 다수의 프로듀서와 컨슈머가 필요에 따라 개별적인 연결을 가져가는 구조였고, 그 때문에 하나의 시스템만 추가되어도 통신 구조가 기하급수적으로 복잡해졌다. (M * N)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_koiil/post/e245d020-66f3-427a-94b8-0ef400614a4a/image.png" alt=""></p>
<ul>
<li>해결: 메시지와 데이터의 흐름을 중앙에서 관리하는 구조를 만들자! =&gt; 카프카</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_koiil/post/dae02fdf-f38f-46b3-a594-c5100db5f8d0/image.png" alt=""></p>
<h4 id="메세지큐rabbitmq와의-차이점">메세지큐(RabbitMQ..)와의 차이점</h4>
<ul>
<li>높은 처리량<ul>
<li>많은 양의 데이터를 묶음 단위로 처리하는 배치 처리가 가능하다.</li>
<li>파티션 단위를 통해 동일 목적의 데이터를 여러 파티션에 분배하고 데이터를 병렬 처리할 수 있다.</li>
</ul>
</li>
<li>확정성<ul>
<li>수십개의 메세지큐를 제각각 운용하는게 아닌, 카프카 클러스터의 브로커 개수를 자연스럽게 스케일 아웃할 수 있다.</li>
<li>다운타임 없이 확장 가능하다 (때문에 데이터의 양을 예측하기 어려울 때 사용 하기 좋다)</li>
</ul>
</li>
<li>영속성<ul>
<li>데이터를 메모리가 아닌 파일 시스템에 저장한다.</li>
<li>페이지 캐시 메모리 영역을 사용한다. 한 번 읽은 파일 내용을 OS가 사용하는 메모리에 저장하기 때문에 파일시스템을 사용하더라도 처리량이 높다.</li>
</ul>
</li>
<li>고가용성<ul>
<li>3개 이상의 서버로 클러스터를 구성하여 특정 서버에 장애가 발생하더라도 클라이언트 요청을 처리할 수 있다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_koiil/post/11143f70-3d70-48d1-b64e-cfde3a4f35b3/image.png" alt=""></p>
<blockquote>
<p>하둡에 많은 영향을 받았지만 하둡은 &quot;빅데이터&quot;에, 카프카는 &quot;real-time&quot; 에 집중했다.</p>
</blockquote>
<h3 id="카프카-구성요소">카프카 구성요소</h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/8f00bd1f-dd40-4f34-a030-5849efb7826c/image.png" alt=""></p>
<h4 id="프로듀서">프로듀서</h4>
<p><img src="https://velog.velcdn.com/images/_koiil/post/8a27b8a3-da9f-4cd7-9509-9fa651e16246/image.png" alt=""></p>
<p>메세지를 특정 토픽에 발행하는 클라이언트이다.
파티셔너를 통해 어떤 파티션에 발행할지 지정한다.</p>
<ul>
<li>메세지 키가 있을 경우 : 해시값을 통해 동일 파티션에서 처리되도록함</li>
<li>메세지 키가 없을 경우 : Round Robin(default)</li>
</ul>
<p>프로듀서에서 <code>send()</code> 이후 파티셔너를 거친 레코드들은 배치 처리를 위해 프로듀서의 버퍼 메모리 영역에 대기한 후 설정에 정의된 값에 따라 브로커에 전송된다.
때문에 배치의 최소 레코드 수를 만족하지 못하면 linger ms만큼 프로듀서 내에서 대기하게 된다.
이같이 Round Robin으로 인한 불필요한 대기를 개선하기 위해 Sticky Partitioner(하나의 파티션에 레코드를 우선적으로 채워서 카프카로 배치전송) 등을 사용한다. </p>
<p>자세한 내용은 이후 포스트에서 다룬다.</p>
<h4 id="브로커">브로커</h4>
<p><img src="https://velog.velcdn.com/images/_koiil/post/0cfb5fc5-e502-49cf-90dd-f91ef7eee381/image.png" alt=""></p>
<p>카프카의 실질적인 서버이다.</p>
<ul>
<li>프로듀서로부터 메세지를 수신</li>
<li>오프셋 지정</li>
<li>메세지를 디스크에 저장</li>
<li>컨슈머의 읽기 요청에 디스크에 수록된 메세지 전송</li>
</ul>
<p>여러 개의 브로커를 묶어 카프카 클러스터를 구성한다.
클러스터 브로커 중 하나는 클러스터의 컨트롤러가 되어 각 브로커에게 파티션 리더를 분배한다. 컨트롤러는 선착순으로 선출된다.
브로커 1 : 토픽 N 으로 구성할 수 있다.</p>
<h4 id="토픽과-파티션-새그먼트">토픽과 파티션, 새그먼트</h4>
<p><img src="https://velog.velcdn.com/images/_koiil/post/909586ff-a6f6-48fa-9d6f-6c623993c765/image.png" alt=""></p>
<ul>
<li>토픽은 메세지(이벤트)를 분류하는 단위이다.</li>
<li>토픽 1 : 파티션 N 으로 구성할 수 있다. 
메세지의 처리는 추가 순이 아닌 파티션별로 관리되기 때문에, 하나의 토픽이 하나의 파티션만 갖는다면 순서가 보장되지만 여러개의 파티션을 갖는다면 순서가 보장되지 않을 수 있다. (같은 파티션 내에서는 순서가 보장된다)</li>
<li>토픽으로 들어오는 메세지는 <code>append only &quot;file&quot;</code> 이다.
컨슈머가 메세지를 소비하더라도 삭제되지 않고 남아있다가 config 에 따라 일정 시간 뒤에 삭제된다.</li>
<li>각 파티션 내의 메시지는 세그먼트라는 단위로 관리되어, 세그먼트가 일정 크기에 도달하면 새로운 세그먼트가 생성된다.<ul>
<li>데이터를 삭제하는 경우에도 세그먼트 단위로 이루어지기 때문에 특정 데이터 삭제는 불가능하다.</li>
</ul>
</li>
<li>파티션의 개수를 잘 설정하는 것이 중요하다.
너무 적을 경우 처리량이 적어지고, 너무 많으면 장애 복구 시간이 늘어난다.</li>
</ul>
<h4 id="메세지">메세지</h4>
<p>바이트 배열의 데이터(기본 데이터 단위)이다.
클러스터는 쓰여진 메시지를 보존기간동안 유지한다.
보존 기간 정책은 <code>log.retention.hours</code> 설정을 통해 가능하며, default는 7일이다.
데이터 크기에 상관없이 카프카의 성능은 일정하기 때문에 장기간 저장해도 문제는 없으므로, 보존기간을 짧게 잡을 필요는 없다.</p>
<h4 id="컨슈머">컨슈머</h4>
<p><img src="https://velog.velcdn.com/images/_koiil/post/cf263562-61b3-46c0-ab7f-c8e3fb5b8b7f/image.png" alt=""></p>
<p>하나 이상의 토픽을 구독하여 메세지를 소비하는 클라이언트이다.
생성된 순으로 메세지를 소비한다. (FIFO)
파티션 단위로 오프셋을 유지하여 위치를 기억한다.</p>
<ul>
<li>commit offset: 처리한 오프셋 위치</li>
<li>current offset: 읽은 오프셋 위치</li>
<li>컨슈머가 읽기를 중단했다가 다시 시작해도 오프셋을 확인해 이어서 메세지 처리 가능하다.</li>
</ul>
<p>여러 대의 컨슈머로 컨슈머 그룹을 형성할 수 있다. </p>
<ul>
<li>하나의 토픽에 여러 컨슈머 그룹이 붙을 수 있다.(어플리케이션 단위로 생각하면 편하다.)</li>
<li>컨슈머 그룹 내의 컨슈머들은 한 토픽의 다른 파티션을 분담하여 읽는다.<ul>
<li>하나의 파티션은 하나의 컨슈머만 처리 가능 (컨슈머가 해당 파티션에 &quot;소유권&quot;을 가진다고 표현한다.)</li>
<li>하나의 컨슈머는 어려 대의 파티션을 담당 가능</li>
<li>때문에 파티션보다 컨슈머가 많으면 컨슈머 stand-by 상태로 놀며 브로커의 TCP Connection 을 낭비하게 된다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_koiil/post/7df57096-58a4-4650-8b83-3bd807e376f6/image.png" alt=""></p>
<p>프로듀서가 마지막으로 추가한 오프셋(Log-end-offset) 과 컨슈머가 마지막으로 읽은 오프셋(current-offset) 의 차이를 컨슈머 랙(consumer LAG)이라고 하며, 모니터링에 중요한 지표가 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 멀티스레딩, 병행성 및 성능 최적화 - 2]]></title>
            <link>https://velog.io/@_koiil/Java-%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%94%A9-%EB%B3%91%ED%96%89%EC%84%B1-%EB%B0%8F-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-2</link>
            <guid>https://velog.io/@_koiil/Java-%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%94%A9-%EB%B3%91%ED%96%89%EC%84%B1-%EB%B0%8F-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-2</guid>
            <pubDate>Sun, 17 Mar 2024 12:00:49 GMT</pubDate>
            <description><![CDATA[<h2 id="성능이란">성능이란?</h2>
<ul>
<li>Latency: 작업 하나의 완료 시간 (시간 단위)</li>
<li>Throughtput: 일정 시간동안 완료한 작업의 양(시간단위당 작업)</li>
</ul>
<h3 id="멀티스레드로-응답시간-최적화">멀티스레드로 응답시간 최적화</h3>
<p>T(싱글 스레드에서의 작업시간)/N(스레드 수)</p>
<ul>
<li>N은 몇 개까지 가능한가?<ul>
<li>코어의 개수가 가장 이상적 (모든 스레드가 인터럽트 없이 실행)</li>
<li>스레드가 하나라도 많아지면 컨텍스트 스위치, 캐시성능저하, 추가메모리 소비를 유발</li>
<li>IO도 블로킹도 없이 모든 스레드가 runnable 상태를 유지하는 것은 비현실적</li>
<li>더불어 요즘은 하이버스레딩을 통해 코어당 두개의 가상스레드로 병렬처럼 실행할 수 있음</li>
</ul>
</li>
<li>각 스레드의 결과를 합치는 코스트<ul>
<li>작업을 세그먼트로 나누는 작업, 스레드 생성비용, 시작비용, 운영체제 스케줄링 비용, 집계완료 시그널 대기 시간, 결과 병합 작업</li>
<li>응답시간 테스트를 통해 트레이트 오프가 필요함</li>
</ul>
</li>
<li>어떤 작업이든 분할이 가능한가?<ul>
<li>Parallelizable task 인지 Sequential task 인지 부분적인지 확인 필요</li>
</ul>
</li>
</ul>
<h3 id="멀티스레드로-처리량-최적화">멀티스레드로 처리량 최적화</h3>
<p>1(시간단위)/T(하나의 작업을 처리하는 시간) -&gt; N/T</p>
<p>subtask 로 쪼개 병렬실행 하는 방법 : N/T 처리량, 지연시간최적화같은 처리량과 무관한 작업이 들어가기 때문에 </p>
<p>별개 스레드에 스케줄링 하는 방법: 작업을 서브테스크로 나누고, 결과를 병합하는 작업이 등이 없어져 최적의 처리량을 낼 수 있음. 매 작업마다 스레드를 재생성할 필요 없음</p>
<h3 id="thread-pooling">Thread pooling</h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/051ff937-2c68-4392-a87f-cca21cef9f8a/image.png" alt=""></p>
<p>따라서 queue와 풀, 적절한 작업 분할을 통해 최적의 처리량과 응답시간을 얻어야함</p>
<h2 id="스레드간-데이터-공유">스레드간 데이터 공유</h2>
<h3 id="stack">stack</h3>
<ul>
<li>메서드가 실행되는 메모리 영역</li>
<li>함수에 전달된 인자, 로컬 변수 등이 저장</li>
<li>호출 순서대로 스택 프레임에 저장되고 메서드가 끝나면 사라짐</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_koiil/post/322fe9b4-f3e7-4dba-a8d9-0f3fe6053a55/image.png" alt=""></p>
<h3 id="heap">heap</h3>
<ul>
<li>Java 내장 클래스 문자열, 객체, 컬렉션, 정적 변수 등</li>
<li>스레드 간 공유</li>
<li>JVM GC 관리 대상</li>
</ul>
<h3 id="stack--heap">stack &amp; heap</h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/7f35b1e1-1036-4794-8951-a42b6138eab2/image.png" alt=""></p>
<h2 id="리소스-공유와-임계영역">리소스 공유와 임계영역</h2>
<ul>
<li>리소스: 데이터, 어떤 상태...<ul>
<li>정수나 문자열 같은 간단한 변수</li>
<li>데이터 구조 배열, 컬렉션, 맵이나 파일을 나타내는 객체</li>
<li>네트워크나 데이터베이스에 대한 연결에 해당하는 객체, 메시지 큐 등</li>
</ul>
</li>
</ul>
<h3 id="공유-리소스의-장점">공유 리소스의 장점</h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/347dd58a-b70f-4ef2-9e17-88d0fed5257a/image.png" alt=""></p>
<p>Queue는 힙에 저장된 공유리소스기때문에 새 작업이 생길 때마다 매번 새 스레드를 다시 만들 필요가 없어 효율적으로 CPU를 활용하고 지연 시간도 늦출 수 있음</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/1d477e5f-ce44-4b9c-bc57-e6095dac5bd5/image.png" alt=""></p>
<p>데이터베이스가 하나뿐이므로 반드시 공유되는 연결이 있어야 하지만 항상 많은 수의 독립된 요청이 유입 가능</p>
<h3 id="임계영역">임계영역</h3>
<p>OS의 스케줄링에 따라 공유자원에 접근하는 여러 스레드의 실행 순서가 달라진다면 의도치 않은 결과가 나올 수 있음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 멀티스레딩, 병행성 및 성능 최적화 - 1]]></title>
            <link>https://velog.io/@_koiil/Java-%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%94%A9-%EB%B3%91%ED%96%89%EC%84%B1-%EB%B0%8F-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-1</link>
            <guid>https://velog.io/@_koiil/Java-%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%94%A9-%EB%B3%91%ED%96%89%EC%84%B1-%EB%B0%8F-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-1</guid>
            <pubDate>Sun, 03 Mar 2024 13:43:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 강의는 글또 X 유데미 콜라보 이벤트로 지원되었습니다. </p>
</blockquote>
<h3 id="why-we-need-threads">Why we need Threads?</h3>
<p>멀티스레드가 필요한 이유</p>
<ul>
<li>응답성(Responsivceness)</li>
<li>성능(Performance)</li>
</ul>
<p>병렬성(concurrency) = 멀티스레드를 이용해 멀티태스킹을 하면 모든 작업이 동시에 실행되는 것 처럼 보인다. </p>
<h4 id="thread-란">Thread 란</h4>
<p>컴퓨터를 켜면</p>
<ul>
<li><p>OS가 디스크에서 메모리로 로드된다.</p>
</li>
<li><p>프로그램(APP)은 디스크에 파일 형태로 저장되어 있다가, 실행되면 OS에 의해 메모리로 올라와 인스턴스(프로세스, application context)로 생성된다.</p>
</li>
<li><p>프로세스 구성 : 프로세스ID(PID), 코드, heap, MainThread(stack+instruction pointer)</p>
<ul>
<li>Stack = 메모리영역, 지역변수가 저장되고 함수가 실행됨</li>
<li>명령어 포린터 = 스레드가 실행할 다음 명령어의 주소를 가지는 그냥 포인터</li>
</ul>
</li>
</ul>
<h3 id="context-switching">Context Switching</h3>
<p>여러 프로세스가 실행되고, 프로세스는 하나 이상의 스레드를 가진다. 그리고 이 스레드들은 CPU 실행을 두고 경쟁하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/e9e8b7f9-76db-4761-a6df-05ee470532d1/image.png" alt=""></p>
<ul>
<li>Stop Thread 1</li>
<li>Schedule Thread 1 out</li>
<li>Schedule Thread 2 in</li>
<li>Start Thread 2</li>
</ul>
<p>CPU에서 실행되는 스레드는 CPU 내 레지스터, 캐시, 메모리의 커널 리소스등을 사용한다.
그래서 컨텍스트 스위칭이 일어나면 작업중이던 스레드의 리소스를 저장하고, 새 스레드의 리소스를 CPU와 메모리에 올리는 작업이 필요하다.
특히 같은 프로세스 내의 스레드들은 공유하는 리소스가 많지만, 다른 프로세스 스레드 간의 컨텍스트 스위칭은 비용이 더 크다.</p>
<h3 id="thread-scheduling">Thread scheduling</h3>
<p>다음과 같은 작업 스레드들이 있을 때, 어떤 순서로 실행해야할까?</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/da9ffd2d-f99b-4868-b59f-eaabba84e097/image.png" alt=""></p>
<ol>
<li>First come
<img src="https://velog.velcdn.com/images/_koiil/post/513d3cd7-5f6a-4847-9dba-43e3b568a172/image.png" alt=""></li>
</ol>
<p>이 경우 긴 요청이 먼저 도착하면 다른 스레드에는 기아현상이 발생한다.
UI 스레드는 실행시간이 짧음에도 응답성이 늦어져 사용자 경험을 저해한다.</p>
<ol start="2">
<li>Shortest Job
<img src="https://velog.velcdn.com/images/_koiil/post/430b0e58-64ce-4bd9-9e41-239b2c997822/image.png" alt=""></li>
</ol>
<p>이 경우 상대적으로 짧은 UI 스레드만 계속해서 실행되고 작업이 있는 긴 스레드는 영원히 실행되지 않을 수 있다.</p>
<ol start="3">
<li>Epochs
<img src="https://velog.velcdn.com/images/_koiil/post/d088be40-3f53-46ad-ab78-5ce07f3e87af/image.png" alt=""></li>
</ol>
<p>OS는 에포크에 맞춰 시간을 적당한 크기로 나눈다.
스레드의 타임 슬라이스를 종류별로 에포크에 할당한다. (모든 스레드가 각 에포크에서 완료되진 않음)</p>
<ol start="4">
<li>Dynamic priority</li>
</ol>
<p>Static priority(개발자가 미리 설정) + Bonus(OS가 각 epoch마다 조절)
OS 는 실시간/인터렉티브 스레드에 우선권을 주고, 기아현상을 막기 위해 이전 에포크에서 완료되지않은 스레드도 확인한다.</p>
<h3 id="thread">Thread</h3>
<p>스레드는 아무것도 하지 않아도 메모리와 일부 커널 리소스를 사용한다.
실행 중인 경우에는 CPU 시간과 CPU 캐시도 사용한다.</p>
<p>때문에 스레드가 오래 걸리거나, 작업을 끝낸 후에도 종료되지 않으면 interrupt 등을 이용해 종료 처리 해야한다.</p>
<h4 id="interrupt">interrupt</h4>
<pre><code class="language-java">public static void main(String [] args) {
    Thread thread = new Thread(new BlockingTask());
    thread.start();
    thread.interrupt();
}
// use InterruptedException
private static class BlockingTask implements Runnable {
    @Override
    public void run() {
        //do things
        try {
            Thread.sleep(500000);
        } catch (InterruptedException e) {
            System.out.println(&quot;Existing blocking thread&quot;);
        }
    }
}

// use isInterrupted()
private static class BlockingTask implements Runnable {
    @Override
    public void run() {
        //do things
        if (Thread.currentThread().isInterrupted()) {
            System.out.println(&quot;Prematurely interrupted computation&quot;);
            return;
        }
        System.out.println(&quot;Existing blocking thread&quot;);
    }
}</code></pre>
<p>인터럽트 신호에 명시적으로 반응하는 메서드가 없을 경우, InterruptedExecption 을 발생시켜 처리해야 한다.</p>
<pre><code class="language-java">    public static void main(String [] args) {
        Thread thread = new Thread(new WaitingForUserInput());
        thread.start();
        thread.interrupt();
    }

    private static class WaitingForUserInput implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    char input = (char) System.in.read();
                    if(input == &#39;q&#39;) {
                        return;
                    }
                }
            } catch (IOException | InterruptedException e) {
                System.out.println(&quot;An exception was caught &quot; + e);
            };
        }
    }</code></pre>
<p>System.in.read() 는 interrupt 에 의해 방해되지 않기 때문에 이건 또 종료가 안된다..</p>
<h4 id="daemon">Daemon</h4>
<p>background 에서 앱을 돌리는 것으로, 스레드의 완료 여부와 상관없이 리턴한다.</p>
<pre><code class="language-java"> public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new LongComputationTask(new BigInteger(&quot;200000&quot;), new BigInteger(&quot;100000000&quot;)));

        thread.setDaemon(true);
        thread.start();
        Thread.sleep(100);
        thread.interrupt();
    }</code></pre>
<h4 id="join">join</h4>
<p>Thread1과 Thread2의 실행 순서를 통제해야하는 경우 Thread.join() 을 쓸 수 있다.</p>
<pre><code class="language-java">// 가능은 하나 비효율적
while(thread.isFinished()) {
    // burn CPU cycles
}

// join 이용
public static void main(String[] args) throws InterruptedException {
        List&lt;Long&gt; inputNumbers = Arrays.asList(100000000L, 3435L, 35435L, 2324L, 4656L, 23L, 5556L);

        List&lt;SomeThread&gt; threads = new ArrayList&lt;&gt;();

        for (long inputNumber : inputNumbers) {
            threads.add(new SomeThread(inputNumber));
        }

        for (Thread thread : threads) {
            thread.setDaemon(true);
            thread.start();
        }

        for (Thread thread : threads) {
            thread.join(2000);
        }

        for (int i = 0; i &lt; inputNumbers.size(); i++) {
            SomeThread thread = threads.get(i);
            if (thread.isFinished()) {
                System.out.println(thread.getResult());
            } else {
                System.out.println(&quot;still in progress&quot;);
            }
        }
    }
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Bean의 범위, 그리고 ObjectProvider와 ProxyMode]]></title>
            <link>https://velog.io/@_koiil/Bean%EC%9D%98-%EB%B2%94%EC%9C%84-%EA%B7%B8%EB%A6%AC%EA%B3%A0-ObjectProvider%EC%99%80-ProxyMode</link>
            <guid>https://velog.io/@_koiil/Bean%EC%9D%98-%EB%B2%94%EC%9C%84-%EA%B7%B8%EB%A6%AC%EA%B3%A0-ObjectProvider%EC%99%80-ProxyMode</guid>
            <pubDate>Thu, 29 Feb 2024 09:52:48 GMT</pubDate>
            <description><![CDATA[<h2 id="스프링에서-지원하는-bean-scope">스프링에서 지원하는 Bean Scope</h2>
<h3 id="singleton"><code>Singleton</code></h3>
<p>기본 스코프(Default Scope)로 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다. (컨테이너가 사라질 때 Bean도 제거된다.)</p>
<p>싱글턴인 경우 현재 스프링 컨테이너에서 같은 종류의 Bean 객체가 오직 1개의 인스턴스만 생성되는 것을 보장하는 디자인 패턴이기 때문에 매 요청마다 같은 객체를 반환한다.</p>
<p>대상 클래스에 <code>@Scope(&quot;singleton&quot;)</code>으로 설정할 수 있다.</p>
<h3 id="prototype"><code>Prototype</code></h3>
<p>스프링 컨테이너가 프로토 타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여하는 짧은 범위의 스코프이다.
따라서 소멸 메서드는 호출하지 않기 때문에 프로토타입 스코프의 객체는 사용자가 직접 자원 해제 코드를 실행해주거나 명시적으로 destroy 메서드를 호출해야 한다.</p>
<p>프로토타입의 경우 클래스의 인스턴스가 spring 컨테이너에 몇 개든 존재할 수 있는 디자인 패턴이다.
즉, 의존 주입이나 <code>getBean()</code> 호출처럼 매번 Bean 객체가 요청될 때마다 항상 새로운 인스턴스를 생성한 뒤 반환해준다.
그렇기 때문에 상태를 유지할 필요가 있는 객체는 Protortype 스코프를, 유지할 필요가 없는 객체는 Singleton 스코프를 적용하는 것이 일반적이다.</p>
<p>대상 클래스에 <code>@Scope(&quot;prototype&quot;)</code>으로 설정할 수 있다.</p>
<hr>
<blockquote>
<p>다음부터 나오는 request, session, global, session은 HTTP 통신과 관련된 스코프기 때문에 스프링으로 웹 기반 애플리케이션(Spring MVC Web Application)을 작성할 때만 적용할 수 있다.</p>
</blockquote>
<p>웹 스코프는 웹 환경에서만 동작하므로 <code>build.gradle</code>에 web 환경, 라이브러리를 추가해야 한다.</p>
<pre><code class="language-java">implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;</code></pre>
<p>만약 해당 라이브러리를 추가하지 않으면  <code>AnnotationConfigApplicationContext</code>를 기반으로 어플리케이션을 구동한다.</p>
<p>웹 라이브러리가 추가되면 웹 관련 기능 및 설정이 필요하기에 <code>AnnotationConfigServletWebServerApplicationContext</code>를 기반으로 애플리케이션을 구동한다.</p>
<h3 id="request"><code>Request</code></h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/097020be-0964-41cf-9ec6-254debfaef3b/image.png" alt=""></p>
<p>HTTP 요청이 들어오고 나갈 때 까지 유지되는 스코프이다.</p>
<p>web-aware 스프링 컨테이너에 전송된 HTTP 요청마다 객체가 생성된다. 동시에 여러 HTTP 요청이 들어왔을 때, 어떤 Request가 남긴 로그인지 판별할 때 사용한다.</p>
<p><strong>주의점</strong>
Request scope는 HTTP 요청이 들어와야 생성되기 때문에 Singleton 빈 의존 관계 주입 시점에는 존재하지 않는다. (자주 ComplieError의 원인이 된다)</p>
<h3 id="session"><code>Session</code></h3>
<p>웹 세션이 생성되고 종료될 때까지 유지되는 스코프이다.
해당 객체는 web-aware 스프링 컨테이너에서 맺어진 HTTP 세션마다 생성된다.
HTTP 요청이 아닌 서버와 클라이언트 간 세션 연결을 기반으로 객체를 생성한다.
마찬가지로 각 세션의 객체들은 서로 변경사항이 반영되지 않는 독립적인 객체들이며 사용이 끝난 뒤 폐기된다.</p>
<h3 id="global-session"><code>Global Session</code></h3>
<p>해당 객체는 web-are 스프링 컨테이너에서 맺어진 global HTTP 세션마다 생성된다.</p>
<h3 id="application"><code>Application</code></h3>
<p>서블릿 컨텍스트(Servlet Context)와 동일한 생명 주기를 가지는 스코프</p>
<h3 id="websocket"><code>Websocket</code></h3>
<p>웹소켓과 동일한 생명주기를 가지는 스코프</p>
<hr>
<h2 id="request-스코프-예제">Request 스코프 예제</h2>
<p>만약 동시에 여러 HTTP 요청이 오면, 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이럴 때 사용하지 좋은 것이 request 스코프이다.</p>
<blockquote>
<p>형식: UUID + requestURL + message</p>
</blockquote>
<p>UUID로 HTTP 요청을 구분하고 어떤 URL을 요청해서 남은 로그인지 확인한다.</p>
<pre><code class="language-java">// CustomLogger.java

@Component
@Scope(value = &quot;request&quot;)
public class CustomLogger {

  private String uuid;
  private String requestURL;

  public void setRequestURL(String requestURL) {
    this.requestURL = requestURL;
  }

  public void log(String message) {
    System.out.println(&quot;[&quot; + uuid + &quot;]&quot; + &quot;[&quot; + requestURL + &quot;] &quot; + message);
  }

  @PostConstruct
  public void init() {
    // 유니크한 아이디 생성
    uuid = UUID.randomUUID().toString();
    System.out.println(&quot;[&quot; + uuid + &quot;] request scope bean created: &quot; + this);
  }

  @PreDestroy
  public void close() {
    System.out.println(&quot;[&quot; + uuid + &quot;] request scope bean closed: &quot; + this);
  }
}</code></pre>
<p>해당 클래스는 request 스코프이기 때문에 HTTP 요청마다 빈이 생성되고 소멸될 것이다.
빈이 생성되는 시점에는 <code>@PostConstruct</code>로 UUID를 생성해 저장해둔다. 빈이 요청 당 하나씩 생성되므로 UUID를 저장해 두면 다른 요청과 구분할 수 있다.
requestURL은 빈이 생성되는 시점에는 알 수 없어 외부에서 setter로 입력받는다.
빈이 소멸되는 시점에는 <code>@PreDestroy</code>를 사용해 종료 메시지를 남긴다.</p>
<pre><code class="language-java">// LogController.java - CustomLogger의 동작을 확인하는 컨트롤러

@Controller
@RequiredArgsConstructor
public class LogController {

  private final LogService logService;
  private final CustomLogger logger;

  @RequestMapping(&quot;log&quot;)
  @ResponseBody
  public String log(HttpServletRequest request) {

    // request를 통해 요청 URL을 받아 setter로 저장했다.
    String requestURL = request.getRequestURL().toString();
    logger.setRequestURL(requestURL);

    //컨트롤러에 controller test 로그를 남기고 logService 의 로직을 호출한다.
    logger.log(&quot;controller test&quot;);
    logService.logic(&quot;testId&quot;);

    return &quot;OK&quot;;
  }
}</code></pre>
<p>reqeustURL을 logger 저장해두면 logger는 HTTP 요청마다 다른 Bean 객체가 생성되므로 고유한 값을 가지게 된다.</p>
<pre><code class="language-java">// LogService.java
@Service
@RequiredArgsConstructor
public class LogService {

  private final CustomLogger logger;

  public void logic(String id) {
    logger.log(&quot;service id = &quot; + id);
  }
}</code></pre>
<p>request 스코프를 사용하지 않고 모든 정보를 서비스 계층에 넘긴다면 파라미터가 많아서 지저분해진다.
requestURL처럼 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가는 문제도 있다.
웹과 관련된 부분은 컨트롤러까지만 사용하고 서비스 계층은 웹 기술에 종속되지 않아야 하므로 가급적 순수하게 유지하는 것이 유지 보수에 좋다.
reqeust 스코프의 CustomLogger 덕분에 웹과 관련된 부분을 파라미터로 넘기지 않고 CustomLogger 멤버 변수에 저장해 코드와 계층을 깔끔하게 유지할 수 있다.</p>
<h3 id="애플리케이션-실행">애플리케이션 실행</h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/fee6814b-a1b5-49a7-bbb5-04407d028dcb/image.png" alt=""></p>
<p>애플리케이션을 실행하면 다음과 에러가 나는 것을 확인할 수 있다.</p>
<p>스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않고 실제 클라이언트의 요청이 와야 생성할 수 있기 때문이다.</p>
<p>자세히 말하자면, 프로젝트가 구동될 때 스프링 빈들이 컴포넌트 스캔이 되며 등록 및 의존관계 주입이 되는데, 여기서 웹스코프인 CustomLogger 빈의 경우 HttpRequest 요청이 올때 생성되는 빈이기 때문에 스프링 구동단계에서는 아직 생성을 할 수 없다. 때문에 빈을 찾을 수 없기에 에러가 발생하는 것이다.</p>
<h3 id="스코프와-objectprovider"><code>스코프와 ObjectProvider</code></h3>
<p>위에서 언급했던 문제를 해결하기 위해 빈 객체를 DL(동적으로 로딩) 할 수 있는 ObjectProvider를 사용해보자.</p>
<pre><code class="language-java">// LogController.java
@Controller
@RequiredArgsConstructor
public class LogController {

    private final LogService logService;
    private final ObjectProvider&lt;CustomLogger&gt; loggerProvider;

    @RequestMapping(&quot;log&quot;)
    @ResponseBody
    public String log(HttpServletRequest request) {
        // DL로 원하는 객체를 가져온다
        CustomLogger logger = loggerProvider.getObject();
        logService.logic(&quot;testId&quot;);

        String requestURL = request.getRequestURL().toString();
        logger.setRequestURL(requestURL);

        logger.log(&quot;controller test&quot;);
        logService.logic(&quot;testId&quot;);

        return &quot;OK&quot;;
    }
}</code></pre>
<pre><code class="language-java">// LogService.java
@Service
@RequiredArgsConstructor
public class LogService {
    private final ObjectProvider&lt;CustomLogger&gt; loggerProvider;

    public void logic(String id) {
        CustomLogger logger = loggerProvider.getObject();
        logger.log(&quot;service id = &quot; + id);
    }
}</code></pre>
<p>ObjectProvider 덕분에 <code>ObjectProvider.getObject()</code>를 호출할 시점까지 request scope 빈을 생성해달라고 스프린 컨테이너에 요청하는 시점을 지연할 수 있다.
따라서 <code>ObjectProvider.getObject()</code>를 호출하는 시점에는 HTTP 요청이 진행 중이므로 request 스코프 빈이 생성된다.</p>
<h3 id="스코프와-프록시">스코프와 프록시</h3>
<p>스코프 속성을 이용해 스프링 빈을 프록시 객체로 만들어 줄 수 있다.</p>
<pre><code class="language-java">// CustomLogger.java
@Component
// 프록시 모드 추가
@Scope(value = &quot;request&quot;, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CustomLogger {
  ...
}</code></pre>
<p>proxyMode를 추가하면 CustomLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입한다.</p>
<p>해당 빈이 실제 사용될 때 프록시 빈에서 실제 빈을 가져와 사용할 수 있도록 한다.</p>
<blockquote>
<p>적용 대상이 클래스면 <code>TARGET_CLASS</code>, 인터페이스면 <code>INTERFACES</code>를 선택한다.</p>
</blockquote>
<pre><code class="language-java">// LogController.java
@Controller
@RequiredArgsConstructor
public class LogController {

    private final LogService logService;
    private final ObjectProvider&lt;CustomLogger&gt; loggerProvider;

    @RequestMapping(&quot;log&quot;)
    @ResponseBody
    public String log(HttpServletRequest request) {
        // DL로 원하는 객체를 가져온다
        CustomLogger logger = loggerProvider.getObject();
        logService.logic(&quot;testId&quot;);

        String requestURL = request.getRequestURL().toString();
        logger.setRequestURL(requestURL);

        logger.log(&quot;controller test&quot;);
        logService.logic(&quot;testId&quot;);

        return &quot;OK&quot;;
    }
}</code></pre>
<pre><code class="language-java">// LogService.java
@Service
@RequiredArgsConstructor
public class LogService {

  private final CustomLogger logger;

  public void logic(String id) {
    logger.log(&quot;service id = &quot; + id);
  }
}</code></pre>
<h3 id="웹-스코프와-프록시의-동작-원리">웹 스코프와 프록시의 동작 원리</h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/6d0140ef-f5e4-44d6-92d0-b7ee697fadc3/image.png" alt=""></p>
<p><code>proxyMode = ScopedProxyMode.TARGET_CLASS</code>를 설정하면 스프링 컨테이너는 CGLIB이라는 바이트 코드 조작 라이브러리로 CustomLogger를 상속받은 가짜 프록시 객체를 생성한다.
스프링 컨테이너에 logger라는 이름의 가짜 프록시 객체를 넣어두고 실제 필요한 시점에 가져와서 동작하는 것이다.
ac.getBean(&quot;logger&quot;, CustomLogger.class)로 찍어봐도 가짜 프록시 객체가 주입된다. 그래서 의존관게 주입도 이 가짜 프록시 객체로 주입된다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>Provider와 프록시의 핵심은 진짜 객체 조회를 곡 필요한 시점까지 지연 처리한다는 점이다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@Transactional(readOnly = true) 꼭 써야하나요?]]></title>
            <link>https://velog.io/@_koiil/TransactionalreadOnly-true-%EA%BC%AD-%EC%8D%A8%EC%95%BC%ED%95%98%EB%82%98%EC%9A%94</link>
            <guid>https://velog.io/@_koiil/TransactionalreadOnly-true-%EA%BC%AD-%EC%8D%A8%EC%95%BC%ED%95%98%EB%82%98%EC%9A%94</guid>
            <pubDate>Sun, 18 Feb 2024 14:45:52 GMT</pubDate>
            <description><![CDATA[<p>이전에 Transactional 에 대한 글을 정리하면서 readOnly에 대해 간략하게 다뤄보았다.
짧게 얘기하자면 <code>@Transactional(readOnly=true)</code>로 설정할 경우 flushmode가 manual로 설정되고 더티체킹을 위한 영속성 컨텍스트 관리 등을 생략하기 때문에 성능을 개선할 수 있다는 내용이었다.</p>
<p>그런데 그럴거면 그냥 트랜잭션 자체를 잡지 않아서 생성 비용을 줄이는게 낫지 않나?
Master-Slave 분기도 사실 따로 세팅해야 적용되니 <code>@Transactional(readOnly=true)</code> 자체는 크게 하는게 없지 않나? 라는 생각에서 아래 내용을 찾아보게 되었다.</p>
<h3 id="테스트해보자">테스트해보자</h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/0d4a58d0-88dd-4943-bf14-4df482a47cb7/image.png" alt=""></p>
<blockquote>
<p><code>Transactional(readOnly=true)</code></p>
</blockquote>
<pre><code class="language-java">... o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [...UserService.getUserWithTransaction]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
... o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(1965184021&lt;open&gt;)] for JPA transaction
... o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@46199519]
Hibernate: 
    select
        u1_0.nickname 
    from
        users u1_0 
    where
        u1_0.nickname=?
... o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
... o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1965184021&lt;open&gt;)]
... o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(1965184021&lt;open&gt;)] after transaction</code></pre>
<blockquote>
<p><code>no @Transactional</code></p>
</blockquote>
<pre><code class="language-java">... tor$SharedEntityManagerInvocationHandler : Creating new EntityManager for shared EntityManager invocation
Hibernate: 
    select
        u1_0.nickname 
    from
        users u1_0 
    where
        u1_0.nickname=?</code></pre>
<p>EntityManaging 같은 경우는 예상대로 동작하기는 했다.
데이터 모수가 작기는 했으나 실행 시간도 쪼끔의 차이가 있었다.
그럼 이제 그럼에도 불구하고 <code>@Transactional(readOnly=true)</code>를 써야하는 이유를 알아보기 위해 어떤 장점들이 있는지 더 알아보자.</p>
<hr>
<h3 id="transactionalreadonlytrue를-쓰면-적용되는-내용들이-뭐길래"><code>@Transactional(readOnly=true)</code>를 쓰면 적용되는 내용들이 뭐길래?</h3>
<ol>
<li>hibernate의 세션 플러시 모드가 <code>FlushType.MANUAL</code>로 설정되어 Persistence Provider 가 EntityManager 를 닫을 때 dirty checking을 건너뛸 수 있도록 한다. (cpu 절약)</li>
<li><code>org.hibernate.readOnly</code>가 <code>true</code> 로 설정되므로 영속성 컨텍스트는 스냅샷을 보관하지 않는다. (메모리 절약)</li>
<li>readOnly queryhint 적용을 통한 DB 레벨에서의 최적화가 이루어진다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/_koiil/post/42312775-58fa-4767-85b7-a7163583038f/image.png" alt=""></p>
<p><a href="https://vladmihalcea.com/spring-read-only-transaction-hibernate-optimization/">Spring read-only transaction Hibernate optimization</a>
<a href="https://stackoverflow.com/questions/44984781/what-are-advantages-of-using-transactionalreadonly-true">What are advantages of using @Transactional(readOnly = true)?</a>
<a href="https://medium.com/@jkha7371/is-transactional-readonly-true-a-silver-bullet-1dbf130c97f8">Is @Transactional(readOnly=true) a silver bullet?</a>
<a href="https://www.inflearn.com/questions/31497/queryhint%EC%9D%98-readonly-%EC%99%80-transaction%EC%9D%98-readonly-%EC%B0%A8%EC%9D%B4">@QueryHint의 readOnly 와 @Transaction의 readOnly 차이</a><br><a href="https://www.inflearn.com/questions/7185/transactional-readonly-true-%EC%97%90%EB%8C%80%ED%95%9C-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4"><code>@Transactional(readOnly = true)</code>에 대한 질문입니다.</a></p>
<hr>
<p>메모리나 성능 상에 큰 메리트가 있는 것도 아니고, 상황에 따라 readOnly 를 통해 설정되는 장점들도 많기 때문에 <code>@Transactional(readOnly=true)</code> 는 단순 단건 조회 등의 케이스를 제외하고는 웬만하면 붙이는 것이 좋을 것 같다.</p>
<ul>
<li>Connector 과 DB Vendor 에 따라 어느 정도의 최적화가 이루어진다. (query hint를 통한 readOnly 설정, aurora의 경우 따로 설정 없이도 master-slave 쿼리 분산 등)<ul>
<li>ex. <a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-performance-ro-txn.html">[MySQL] 10.5.3 Optimizing InnoDB Read-Only Transactions</a></li>
</ul>
</li>
<li>DB 에 따라 테이블 잠금을 생략하거나 실수로 트리거할 수 있는 쓰기 작업을 거부하는 용도로 사용할 수 있다.</li>
<li>한 메서드 내에서 여러 조회를 하는 경우 매 select 쿼리마다 commit 하는 것을 방지할 수 있다. (1차 캐시는 보관하므로)</li>
<li>OSIV=false 인 경우 Transactional을 붙이지 않으면 조회 후 바로 준영속 상태가 되기 때문에 Lazy Loading을 시도할 경우 LazyInitializationException이 발생한다.</li>
<li>아래와 같은 상황을 방지할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_koiil/post/6599a8b4-ce23-47cd-912a-56dc1ae6224d/image.png" alt=""></p>
<p>그리고 각 DB 의 readOnly 플래그에 대한 동작을 야물딱지게 정리한 글이 있어서 첨부하며 마무리한다.</p>
<p><a href="https://lob-dev.tistory.com/entry/DBMS-%EB%B3%84-Transaction-Read-Only%EC%97%90-%EB%8C%80%ED%95%9C-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D-1">DBMS 별 Transaction Read Only에 대한 동작 방식</a></p>
<h3 id="todo">TODO</h3>
<p>Transactional 설정으로 인한 isolation level, propagation 기본값이 transactional이 없을 땐 어떻게 되는지?</p>
<h3 id="appendix">Appendix.</h3>
<p><a href="https://www.baeldung.com/spring-transactions-read-only">[Baeldung] Using Transactions for Read-Only Operations</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis pipelining 을 통한 알림 서비스 개선기]]></title>
            <link>https://velog.io/@_koiil/Redis-pipelining</link>
            <guid>https://velog.io/@_koiil/Redis-pipelining</guid>
            <pubDate>Sun, 24 Dec 2023 08:04:46 GMT</pubDate>
            <description><![CDATA[<h3 id="1-발단"><code>1. 발단</code></h3>
<p>기존의 알림 서비스는 아래와 같이 구성되어 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/578dc183-6bcf-4c68-ab2f-e65015e77653/image.png" alt=""></p>
<ol>
<li>각 서비스 서버에서 알림이 필요한 이벤트가 발생하면 알림 서버로 요청을 보냄</li>
<li>알림 서비스는 레디스에서 유저의 알림 설정 정보를 조회해옴</li>
<li>알림 타입별(광고/결제/소셜..) 설정이 ON 인 유저만 필터링</li>
<li>메세지 및 링크 생성 등의 처리 후 푸시 서버로 발송 요청</li>
</ol>
<p>기존에도 소셜 알림의 경우 한번에 1000+명의 유저에게 발송이 필요했지만, 큰 이슈 없이 동작했기 때문에 발견되지 않았습니다.</p>
<p>그런데 이벤트 기간동안 몇 십만명에게 푸시를 발송해야하는 요구사항이 있었고, response time 이 7초까지 지연되며 개선의 필요성이 화두에 올랐습니다.</p>
<h3 id="2-원인-파악">2. 원인 파악</h3>
<p><img src="https://velog.velcdn.com/images/_koiil/post/e6417fa4-2fbc-481a-8816-e3f7f4e22af0/image.png" alt=""></p>
<p>로그와 핀포인트를 통해 확인한 결과, 성능 저하의 원인이 되는 부분은 명확했습니다.
유저의 알림 설정 정보를 가져오는 부분에서 1건씩 조회를 하며 많은 시간을 소요하고 있었습니다.</p>
<pre><code class="language-java">
public List&lt;Long&gt; getNotificationEnabled(List&lt;Long&gt; memberList,
                                                  Integer notificationNo) {
          ...                                               
        return memberList.stream()
                  .map(this::findById)  // 한 건씩 조회
                  .filter(it -&gt; it.doSomething())
                  .map(Notification::getMemberNo)
                  .toList();
}  

public Optional&lt;Notification&gt; findById(Long memberNo) {
        return Optional.of(hashOperations.entries(memberNo))
                       .filter(Predicate.not(Map::isEmpty))
                       .map(Notification::of);
}</code></pre>
<p>각 멤버에 대한 Redis 조회가 <code>stream().map()</code> 내부에서 개별적으로 이루어졌기 때문에 findById 메서드가 매번 호출되어 Redis 연결이 생성되고 종료되는 문제가 발생했습니다.</p>
<h3 id="원인-분석">원인 분석</h3>
<p>기존처럼 stream 을 돌며 조회를 했을 때의 로그는 아래와 같습니다.</p>
<pre><code class="language-java">[           main] : [channel=0x36802467] write() writeAndFlush command AsyncCommand [type=HGETALL]
[ueEventLoop-7-1] : [channel=0x36802467] write(ctx, AsyncCommand [type=HGETALL], promise)
[ueEventLoop-7-1] : [channel=0x36802467] writing command AsyncCommand [type=HGETALL]
[           main] : [channel=0x36802467] write() done
[ueEventLoop-7-1] : [channel=0x36802467] Received: 4 bytes, 1 commands in the stack
[ueEventLoop-7-1] : [channel=0x36802467] Stack contains: 1 commands
[ueEventLoop-7-1] : Decode done, empty stack: true
[ueEventLoop-7-1] : [channel=0x36802467] Completing command LatencyMeteredCommand [type=HGETALL]
[           main] : dispatching command AsyncCommand [type=HGETALL]
[           main] : [channel=0x36802467] write() writeAndFlush command AsyncCommand [type=HGETALL]
[ueEventLoop-7-1] : [channel=0x36802467] write(ctx, AsyncCommand [type=HGETALL], promise)
[ueEventLoop-7-1] : [channel=0x36802467] writing command AsyncCommand [type=HGETALL]
[           main] : [channel=0x36802467] write() done
[ueEventLoop-7-1] : [channel=0x36802467] Received: 4 bytes, 1 commands in the stack
[ueEventLoop-7-1] : [channel=0x36802467] Stack contains: 1 commands
[ueEventLoop-7-1] : Decode done, empty stack: true
[ueEventLoop-7-1] : [channel=0x36802467] Completing command LatencyMeteredCommand [type=HGETALL]</code></pre>
<ul>
<li>모든 명령이 동일 channel을 통해 전달되지만</li>
<li><code>write() done</code> 이후 다음 명령이 시작되는 동기식 패턴이 보임</li>
<li>5000건 기준 3분 이상 소요</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_koiil/post/0c5ceb6c-47ce-47ee-bf43-69e45013bfe0/image.png" alt=""></p>
<h3 id="해결-방안-검토">해결 방안 검토</h3>
<ol>
<li>⛔️ 인자를 리스트로 받는 쿼리를 통해 다건조회</li>
</ol>
<p>일반 <code>StringRedisTemplate</code> 에서는 mget을 통해 쉽게 멀티 키 조회가 가능합니다.
허나 현재 서비스는 hash 형식으로 데이터를 저장하고 있었고, <code>HashOperation</code> 에서는 멀티 키 조회를 지원하지 않고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/7c98fcf6-57fa-46d1-89c9-d33aa92f0e8a/image.png" alt=""></p>
<ol start="2">
<li>⛔️ Lettuce ReactiveRedisTemplate 사용</li>
</ol>
<p>이는 Lettuce 의 비동기를 가장 잘 활용하는 방법이지만, 리액티브 프로그래밍 경험이 부족한 팀 상황에서 러닝 커브가 높고 유지보수 리스크도 있을 것이라 판단하여 다른 방법을 채택했습니다.
전체 애플리케이션이 리액티브하지 않은 상황에서 부분 적용을 하는 것보다, 이후 방식을 통한 처리가 합리적이었습니다.</p>
<pre><code class="language-java">ReactiveRedisTemplate&lt;String, String&gt; reactiveTemplate = ...;
return reactiveTemplate.opsForHash()
    .entries(key)
    .flatMap(...) // non-blocking
    .collectList()
    .block(); // block (if needed)</code></pre>
<ol start="3">
<li>✅ 커넥션을 생성하여 조회</li>
</ol>
<p>Redis 는 기본적으로 Request/Response 프로토콜을 사용하며, 하나의 요청에 대한 응답을 받은 후에 다음 요청을 처리합니다. 이는 Round-Trip Time (RTT)을 증가시켜 전체 응답 시간에 영향을 미칠 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/891b58cc-fca2-4aa3-9f97-e8b218be9a12/image.png" alt=""></p>
<p>이를 해결하기 위한 방법으로 <a href="https://redis.io/docs/manual/pipelining/">Redis</a> 와 <a href="https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html">Spring Data Redis 공식문서</a> 에서 Pipelining에 대한 내용을 확인할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/1930fc42-581d-40df-bfd9-22b269eb24ec/image.png" alt=""></p>
<p>조회를 하는 부분의 코드를 아래와 같이 pipeline 처리를 했고, 성능 테스트 결과 10000 건 조회에 걸리는 시간이 약 1/100 수준으로 줄어든 것을 확인할 수 있었습니다.</p>
<pre><code class="language-java">@SuppressWarnings(&quot;unchecked&quot;)
private Map&lt;Long, Notification&gt; getMemberNotificationMap(List&lt;Long&gt; memberNos) {
    return hash.getOperations().executePipelined(new SessionCallback&lt;&gt;() {
                   @Override
                   public &lt;K, V&gt; Object execute(RedisOperations&lt;K, V&gt; operations) throws DataAccessException {
                       HashOperations&lt;K, Object, Object&gt; hashOperations = operations.opsForHash();
                       memberNos.forEach(memberNo -&gt; hashOperations.entries((K) getKey(memberNo)));
                       return null;
                   }
               })
               .stream()
               .map(it -&gt; (Map&lt;String, String&gt;) it)
               .filter(Predicate.not(Map::isEmpty))
               .map(Notification::of)
               .collect(Collectors.toMap(Notification::getMemberNo, Function.identity()));
}</code></pre>
<p>아래는 파이프라이닝을 적용했을 때의 로그입니다.
응답을 기다리지 않고 명령을 모두 전송한 후, 응답이 돌아오는대로 받는 것을 확인할 수 있습니다.</p>
<pre><code class="language-java">
[           main] :  write() writeAndFlush command AsyncCommand [type=HGETALL]
[ueEventLoop-7-2] :  writing command AsyncCommand [type=HGETALL]
[           main] :  write() done
[ueEventLoop-7-2] :  write(ctx, AsyncCommand [type=HGETALL], promise)
[           main] :  command AsyncCommand [type=HGETALL]
[           main] :  write() writeAndFlush command AsyncCommand [type=HGETALL]
[ueEventLoop-7-2] :  writing command AsyncCommand [type=HGETALL]
[           main] :  write() done
[ueEventLoop-7-2] :  write(ctx, AsyncCommand [type=HGETALL], promise)
[           main] :  command AsyncCommand [type=HGETALL]
    ...
[ueEventLoop-7-2] : Completing command LatencyMeteredCommand [type=HGETALL]
[ueEventLoop-7-2] : Stack contains: 1015 commands
[ueEventLoop-7-2] : Decode done, empty stack: true
[ueEventLoop-7-2] : Completing command LatencyMeteredCommand [type=HGETALL]
[ueEventLoop-7-2] : Stack contains: 1014 commands
[ueEventLoop-7-2] : Decode done, empty stack: true</code></pre>
<p><img src="https://velog.velcdn.com/images/_koiil/post/828db2a6-ee02-464a-8f83-b182585e0823/image.png" alt=""></p>
<h3 id="appendix">Appendix</h3>
<p>현재 알림 서비스의 구조는 아래처럼 prefix 와 조합해 키를 생성하기 때문에 레디스 클러스터 구성에서도 모든 알림 데이터가 동일 슬롯에 저장되고 있습니다.</p>
<pre><code class="language-java">// pipelining method
    ...
    memberNos.forEach(memberNo -&gt; hashOperations.entries((K) getKey(memberNo)));
    ...

// getKey()
public String getKey(Long memberNo) {
    return &quot;notification:&quot; + memberNo;
}</code></pre>
<p>하지만 이 말은, 키가 여러 슬롯에 분산되는 상황이 된다면 <code>CROSSSLOT Keys</code> 에러가 발생할 수 있다는 말이기도 합니다.
때문에 이 경우는 슬롯별로 그룹화하여 여러 번의 파이프라인 실행이 필요합니다.</p>
<hr>
<p>현재 단순 일괄 처리이기에 파이프라이닝으로 적용했지만, 추후 복잡한 비동기 처리 필요 시 <strong>Lettuce native API 사용</strong> 또한 고려해볼 수 있겠습니다.</p>
<pre><code class="language-java">// 비동기 명령 실행
RedisAsyncCommands&lt;String, String&gt; async = connection.async();
RedisFuture&lt;Map&lt;String, String&gt;&gt; future = async.hgetall(key);

// 결과 처리
LettuceFutures.awaitAll(timeout, TimeUnit.SECONDS, future);</code></pre>
<p>RedisAsyncCommands 를 사용하면 여러 조회 작업을 독립적으로 실행, 관리할 수 있고 중간 결과를 활용할 수 있습니다.</p>
<h3 id="ref">Ref.</h3>
<p><a href="https://redis.io/docs/manual/pipelining/">Redis pipelining</a>
<a href="https://stackabuse.com/spring-boot-with-redis-pipeline-operations/">Spring Boot with Redis: Pipeline Operations</a>
<a href="https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html">Spring Data Redis Pipelining</a>
<a href="https://lettuce.io/core/release/reference/#_pipelining_and_command_flushing">lettuce.io/pipelining_and_command_flushing</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] 글또 8기를 마치며]]></title>
            <link>https://velog.io/@_koiil/%ED%9A%8C%EA%B3%A0-%EA%B8%80%EB%98%90-8%EA%B8%B0%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0</link>
            <guid>https://velog.io/@_koiil/%ED%9A%8C%EA%B3%A0-%EA%B8%80%EB%98%90-8%EA%B8%B0%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0</guid>
            <pubDate>Sun, 16 Jul 2023 12:18:08 GMT</pubDate>
            <description><![CDATA[<p>연초에 우연한 기회로 참여하게된 글또가 6개월의 여정을 마치고 마무리됐다.
이 글은 글또 8기 활동을 마무리하는 마지막 글이다!🎊
사실 마지막 글을 회고글로 제출하고싶지는 않았는데, 작성 중이던 글이 생각보다 오오오래걸려서...😂 </p>
<p><a href="https://velog.io/@_koiil/%EA%B8%80%EB%98%90-8%EA%B8%B0%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0">[회고] 글또 8기를 시작하며</a>를 시작으로 패스 2회와 벌써 9개의 글을 작성했다.
생각보다 6개월 새에 공유해볼만한 트러블 슈팅도 있었고, 기술적으로 궁금했던 내용을 찾아볼 원동력도 되어주어 재밌게 글또 활동을 마무리할 수 있었다.</p>
<hr>
<h3 id="목표는-달성했나">목표는 달성했나?</h3>
<p>당초에 글또를 시작하며 목표로 잡았던 것들은 아래와 같다.</p>
<ol>
<li>트러블슈팅과 설계, 개발 과정을 더 많이 기록해두고싶다.</li>
<li>개발일기에 더 많은 글을 써보고자 한다.</li>
<li>위 글들을, 읽기 편하게 쓸 수 있게 되고싶다.</li>
</ol>
<p>1번은 꽤 마음에 차게 달성한 것 같다. 언젠간 알아봐야지 했던 GC, 메모리, 캐시, json 등에 대해 다뤄봤고 정리하며 찾아보는 과정 자체가 스스로한테도 많은 도움이 되었다.
아직도 쓰려고 올려둔 비공개 글들이 많은데, 기회가 된다면 9기에서 마저 제출해보고싶다.</p>
<p>2번 또한, 올해는 경험은 적었지만 새로운 책을 많이 샀고, 그로부터 생각하게 되는 것들이 꽤 있었다. 아직 短想 으로 정리 중이지만, 언젠가 어엿한 나의 표현으로 타인들에게 공유할 수 있으면 좋겠다.</p>
<p>3번은 달성했는지 잘 모르겠다. 문맥에 대해 불편하다는 피드백은 없었지만, 조금 더 피드백이 많았으면 알 수 있지 않았을까?</p>
<p>결론적으로 100프로 달성했다고 확신있게 말하진 못하지만, 기대보다 더욱 재밌고 유익한 활동이었던것같다.</p>
<hr>
<h3 id="글또-후기">글또 후기</h3>
<p>누군가 글또를 할지 고민중이라고 한다면, 나는 무조건 추천할 것이다!</p>
<p>글또 외에도 매주 글을 쓰고 공유하는 등의 스터디는 어디에나 있지만, 글또를 해본 결과 격주에 한번도 굉장히...지키기 어려웠기 때문에 이정도 텀이 적당하다고 생각한다.
또한 커피드백 활동을 통해 새로운 사람들과 커뮤니티를 형성할 수 있다는것도 굉장히 매력적인 활동이었다.</p>
<p>본질적으로 해보고싶었던 <strong>글에 대한 피드백</strong>은 솔직히 글또에서 강요하는 것은 아니기 때문에 적극적으로 이루어지는 편은 아니라고 본다. 
하지만 글또 활동을 진행하며 친해지게 되는 사람들은 서로의 글을 찾아보고, 궁금한 점 혹은 피드백을 개인적으로 주시기도 해서, 결론적으로는 어느정도 충족이 된 듯 하다.</p>
<p>그리고 글또의 가장 좋은 점은, 스터디와 모임이 굉장히 활성화되어있는 것이라고 생각한다.</p>
<p>글또에서 <a href="https://learing-is-vital-in-development.notion.site/LIVID-7076f8b79aa4409f8572d8f2c8634bc7">LIVID</a> 라는 스터디에 참여하게 되었는데, 멤버나 주제나 굉장히 만족하고 있다.
이전에는 <a href="https://www.yes24.com/Product/Goods/102819435">가상 면접 사례로 배우는 대규모 시스템 설계 기초</a>라는 책으로 진행했고, 현재는 <a href="https://www.yes24.com/Product/Goods/59566585">데이터 중심 어플리케이션 설계</a> 로 진행 중인데 아... 난이도가 상당해서 열심히 살게 된다😂😂
이게 최소한 격주로 글을 제출할만큼 성실하고 열정적인 사람들이 모여있다보니, 스터디를 꾸려도 잘 굴러가는 듯 하다.</p>
<p>또한 미루고 미뤘던 포폴 스터디도 진행했는데, 서로 피드백을 주고 받으며 이제 어느정도 완성된 포맷을 갖게 되었다! 이번에 수정한 포폴로는 서류 합격률이 상당히 괜찮았다.
그래도 다른 사람들 이력서를 보니...앞으로도 정말 많이 노력해야겠다 싶었다.</p>
<p>다음 기수도 활동할 수 있다면 좋겠다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ObjectMapping 라이브러리 비교]]></title>
            <link>https://velog.io/@_koiil/ObjectMapping-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@_koiil/ObjectMapping-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Sun, 04 Jun 2023 10:24:31 GMT</pubDate>
            <description><![CDATA[<p>Entity to DTO, JSON to DTO 등 객체간 매핑 작업이 필요할 때가 있습니다.</p>
<p>필드가 적을 때는 생성자를 통해 일일히 매핑할 수 있지만, 개발이 늘 해피할 수는 없는 법...
30개가 넘게 늘어나는 경우도 부지기수고 직접 매핑하는 과정에서의 휴먼 에러를 무시할 수 없기 때문에 필드를 통한 매핑 라이브러리를 사용합니다.</p>
<p>기본적으로 자주 사용되는 <code>ObjectMapper</code>,<code>ModelMapper</code>, <code>MapStruct</code> 세 가지를 비교해보고, 어떤 것을 사용하는 것이 좋을 지 알아보겠습니다.</p>
<pre><code class="language-java">import com.fasterxml.jackson.databind.ObjectMapper;
import org.modelmapper.ModelMapper;
import org.mapstruct.factory.Mappers;


class Person {  // Source class 도 동일
    private String name;
    private int age;
}

public void map() {
      // Source 객체 생성
      Source source = new Source(&quot;Koiil&quot;, 26);

      // ObjectMapper
      ObjectMapper objectMapper = new ObjectMapper();
      Person person = objectMapper.convertValue(source, Person.class);

      // ModelMapper
      ModelMapper modelMapper = new ModelMapper();
      Person person = modelMapper.map(source, Person.class);

      // MapStruct
      PersonMapper mapper = Mappers.getMapper(PersonMapper.class);
      Person person = mapper.sourceToPerson(source);
}</code></pre>
<h4 id="objectmapper">ObjectMapper</h4>
<p><code>ObjectMapper</code>는 Jackson 라이브러리의 일부로 제공되며, 주로 JSON-Object 변환을 수행하는 데 사용합니다.
spring-boot-starter-web 에 포함되어있기 때문에, 일반적인 spring web application 프로젝트에서는 따로 라이브러리를 추가할 필요가 없습니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/d00142d2-e535-468c-8519-0496144471fc/image.png" alt=""></p>
<p>하지만 개복치처럼 뭐 하나라도 없으면 에러를 내뱉는 라이브러리입니다.
기본생성자와 setter(혹은 getter) 가 무조건 필요하고, 하나라도 없을 시 에러가 발생합니다.</p>
<pre><code class="language-java">// No @NoArgsConstructor
Cannot construct instance of `...Person` (no Creators, like default constructor, exist): 
cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: UNKNOWN; byte offset: #UNKNOWN]

// No @Setter
Unrecognized field &quot;name&quot; (class ...Person), not marked as ignorable (0 known properties: ])
 at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: ...Person[&quot;name&quot;])
</code></pre>
<blockquote>
<p><code>ObjectMapper</code> 는 target 객체에 setter 가 아닌 getter 만 있어도 값을 정상적으로 매핑할 수 있는데, 그 이유는 <a href="https://jenkov.com/tutorials/java-json/jackson-objectmapper.html#how-jackson-objectmapper-matches-json-fields-to-java-fields">How Jackson ObjectMapper Matches JSON Fields to Java Fields</a> 에서 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/_koiil/post/9a1f6015-f3e9-4edd-90fe-bba62d8c3227/image.png" alt=""></p>
</blockquote>
<h4 id="modelmapper">ModelMapper</h4>
<p><code>ModelMapper</code>는 객체 간의 매핑을 수행하는 데 사용되는 라이브러리로, 필드 이름이 동일하면 자동으로 매핑을 수행합니다.
<a href="http://modelmapper.org/user-manual/providers/">공식문서</a> 에서 확인할 수 있듯이, 기본 생성자를 필요로 하기 때문에 @NoArgsConstructor 가 없으면 에러를 발생시킵니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/0b0c02f3-ad52-491c-b83a-91f22d1f4a62/image.png" alt=""></p>
<pre><code class="language-java">/** 성공 **/
@Setter
class Person {
    private String name;
    private int age;
}

/** 실패
Failed to instantiate instance of destination ...Person.
Ensure that ...Person has a non-private no-argument constructor.
**/
@Setter
@AllArgsConstructor
class Person {
    private String name;
    private int age;
}
</code></pre>
<p>하지만 <code>ObjectMapper</code> 와 다르게, @Setter가 없어도 기본 생성자로 초기화된 객체를 반환하기 때문에 에러가 발생하지 않습니다.</p>
<h4 id="mapstruct">MapStruct</h4>
<p><a href="https://mapstruct.org/documentation/stable/reference/html/">MapStruct</a>는 코드 생성 기반의 매핑 도구로, 인터페이스에 매핑 규칙을 정의하고, @Mapping 어노테이션을 사용하여 복잡한 필드 간의 매핑 규칙을 정의할 수 있습니다.</p>
<p><code>MapStruct</code>는 는 컴파일 시점에서 최적화된 implememt 된 클래스를 생성하는데, 해당 클래스를 살펴보면 생성자로 사용되는 어노테이션에 따라 매핑 구현이 달라지는 것을 확인할 수 있습니다.</p>
<pre><code class="language-java">import javax.annotation.processing.Generated;

@Generated(
    value = &quot;org.mapstruct.ap.MappingProcessor&quot;,
    date = &quot;2023-06-04T16:47:20+0900&quot;,
    comments = &quot;version: 1.5.3.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.4.1.jar, environment: Java 16.0.1 (AdoptOpenJDK)&quot;
)
public class PersonMapperImpl implements PersonMapper {

    @Override
    public Person sourceToPerson(Source source) {
        if ( source == null ) {
            return null;
        }

        // @Builder 사용시
        Person.PersonBuilder person = Person.builder();

        person.name( source.getName() );
        person.age( source.getAge() );

        return person.build();

        // @NoArgsConstructor 사용시
        Person person = new Person();

        person.setName( source.getName() );
        person.setAge( source.getAge() );

        return person;

        // @AllArgsConstructor 사용시
        Person person = new Person( source.getName(), source.getAge() );

        return person;

    }
}</code></pre>
<p>때문에 위 라이브러리들은 @Setter 와 생성자가 필요했지만, <code>MapStruct</code>는 @Setter 없이도 @AllArgsConstructor 나 @Builder 를 사용해 매핑을 할 수 있습니다.</p>
<hr>
<h3 id="정리">정리</h3>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center">아무것도 없음<br>(@Setter 없음 only 기본생성자)</th>
<th align="center">아무것도 없음<br>(public 필드)</th>
<th align="center">@Setter 만 있음<br>(기본생성자 생성)</th>
<th align="center">@Setter + @AllArgsConstructor<br>(기본생성자 없음)</th>
<th align="center">@Builder 사용</th>
</tr>
</thead>
<tbody><tr>
<td align="center">ObjectMapper</td>
<td align="center">Error</td>
<td align="center">Success</td>
<td align="center">Success</td>
<td align="center">Error</td>
<td align="center">Error</td>
</tr>
<tr>
<td align="center">ModelMapper</td>
<td align="center">Success<br>(기본 객체 생성)</td>
<td align="center">Success<br>(기본 객체 생성)</td>
<td align="center">Success</td>
<td align="center">Error</td>
<td align="center">Error</td>
</tr>
<tr>
<td align="center">MapStruct</td>
<td align="center">Success<br>(기본 객체 생성)</td>
<td align="center">Success</td>
<td align="center">Success</td>
<td align="center">Success</td>
<td align="center">Success</td>
</tr>
</tbody></table>
<p>기본적으로 필드 set 만 가능하다면 동작합니다.(@Setter 혹은 pulic field)</p>
<ul>
<li><code>ObjectMapper</code> : 기본 제공이므로 단순 JSON 데이터 처리에 유용</li>
<li><code>ModelMapper</code> : 간단한 객체 간의 매핑</li>
<li><code>MapStruct</code> : 개별 mapper 생성해주는게 귀찮으나 복잡한 매핑도 가능</li>
</ul>
<p><code>ObjectMapper</code> 와 <code>ModelMapper</code> 은 런타임 매핑과정에서 Reflection을 사용하지만, 
<code>MapStruct</code> 는 컴파일타임에서 어노테이션을 읽어 최적화된 로직을 생성하기 때문에 Reflection을 사용하지 않아 성능상 이점이 있습니다.</p>
<blockquote>
<p>개인적으로 개발할 때 setter 은 지양하는 편이고, builder을 주로 사용하기 때문에 MapStruct 만이 제대로 동작했겠네요..
무심코 어 전엔 됐던거같은데 왜안돼지? 했었는데, 알고보니 MapStruct를 자주 사용하게 되는 이유가 있었습니다.</p>
</blockquote>
<hr>
<h4 id="check-해볼-것">check 해볼 것</h4>
<p><a href="https://github.com/modelmapper/modelmapper/issues/265">ModelMapper should support Builder Pattern
</a></p>
<p>이슈는 closed이고 MR 도 처리됐는데 일단 3.1.1 버전 기준 여전히 동작안하기는 함...</p>
<p><a href="https://d2.naver.com/helloworld/0473330">Jackson의 확장 구조를 파헤쳐 보자</a>
<a href="https://lob-dev.tistory.com/entry/%EA%B0%9D%EC%B2%B4-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0-%EC%9E%90%EB%B0%94-%EC%BD%94%EB%93%9C-%EB%A7%A4%ED%95%91-vs-MapStruct-vs-ModelMapper">자바 코드 매핑 vs MapStruct vs ModelMapper</a>
<a href="https://velog.io/@park2348190/Jackson-ObjectMapper%EC%97%90%EC%84%9C-%EA%B8%B0%EB%B3%B8-%EC%83%9D%EC%84%B1%EC%9E%90-%EC%97%86%EC%9D%B4-Deserialization-%ED%95%98%EA%B8%B0">Jackson ObjectMapper에서 기본 생성자 없이 Deserialization 하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIPS] IntelliJ 사용 꿀팁 🍯]]></title>
            <link>https://velog.io/@_koiil/IntelliJ-%EC%82%AC%EC%9A%A9-%EA%BF%80%ED%8C%81</link>
            <guid>https://velog.io/@_koiil/IntelliJ-%EC%82%AC%EC%9A%A9-%EA%BF%80%ED%8C%81</guid>
            <pubDate>Thu, 27 Apr 2023 10:30:07 GMT</pubDate>
            <description><![CDATA[<h3 id="jshell-console">Jshell Console</h3>
<p><code>Tools &gt; Jshell Console</code> 을 통해 자바 실행기를 이용할 수 있습니다!</p>
<p>library method 동작을 확인할 때나 encrypt, decrypt 를 할 때 등 자바 실행기가 필요할 때 용이합니다!
기존에는 java 파일을 생성하거나 테스트코드로 실행시켯는데, intelliJ 내 실행기가 있으니 활용도가 더 좋은 듯 합니다</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/0bad69f8-cf0e-48a6-8f73-0ec8405d9642/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/0afca912-8963-423c-9f16-8915e9a0982f/image.png" alt=""></p>
<h3 id="before-commit">Before Commit</h3>
<p>commit 탭 하위의 <code>Before Commit</code> 에서 여러 옵션을 선택할 수 있습니다.</p>
<p>sonarlint 로 warning 을 잡거나, unused import 정리, 코드 정렬 등을 수행해주니 켜두는 것을 추천합니다!</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/2b38b509-ca7a-44fa-8990-5935316d5d2e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/cb26f65e-65ce-4531-a742-cb2afbc823c5/image.png" alt=""></p>
<h3 id="stream-chaining">Stream chaining</h3>
<p>Debug 모드로 스트림을 찍은 후, 디버그 콘솔 맨 우측 <code>Trace Current Stream Chain</code> 을 누르면 스트림 단계별로 처리된 결과를 볼 수 있습니다.
split 모드를 하면 각 stream chain 별로, flat 모드로 하면 모든 과정별로 결과값을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/fec72e95-f4b2-4239-adb5-06ed01d0f5d1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/4ca95a9d-4921-4c47-a6bd-1739573effa0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/add4f88a-6d11-4a27-bce9-6c234cb62b0e/image.png" alt=""></p>
<blockquote>
<p>개인적으로 잘 사용하는 기능만 적어놨는데, 더 많은 인텔리제이 사용 팁은 <a href="https://www.youtube.com/@intellijidea">IntelliJ IDEA by JetBrains Youtube</a> 에서 확인할 수 있습니다!
IDE에서 편리하고 강력한 기능을 많이 제공해주니 적극 활용해 효율적으로 일합시다....🔥</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[200 Exception]]></title>
            <link>https://velog.io/@_koiil/200-Exception</link>
            <guid>https://velog.io/@_koiil/200-Exception</guid>
            <pubDate>Mon, 17 Apr 2023 04:50:37 GMT</pubDate>
            <description><![CDATA[<p>사내 몇몇 서비스는 Custom Exception 에 대해 Http Status Code 200, body 내 정의된 result code 로 어떤 오류인지를 내려주고 있다. (심지어 최근까지 뭐를 표준으로 할지 싸웠음)</p>
<p>나도 한때는 &quot;어쨋든 서버랑 통신이 됐으니 200을 줘도 되는거 아냐? 무슨 에러인지는 리턴코드 보면 알 수 있으니까&quot; 라는 생각으로 200 에러 리스폰스를 리턴한 적이 있었다.
말도 안되는 논리다. 오죽하면 위같은 짤도 돌아다닌다.</p>
<p>Status 200 으로 에러를 반환하면 안되는 이유에 대한 생각을 해봤다.</p>
<hr>
<h4 id="클라이언트에서의-처리">클라이언트에서의 처리</h4>
<p>정말 슬프게도 아직도 사내의 몇몇 서비스는 ajax와 jQuery 를 사용한다.
내가 프론트 라이브러리에 대한 이해도가 떨어져 비교분석이 힘든 것도 있지만, 여전히 (꽤나 많이) 사용 중인 저 조합을 기준으로 얘기하고자한다.</p>
<pre><code class="language-javascript">
$.ajax({
  method: &quot;POST&quot;,
  url: sendUrl,
  data: body,
  async: true,
  success: function (res) { handleSuccess(res) },
  error: function (err) { handleFailure(err) },
  complete: function () { loader.hide() }
});
</code></pre>
<p>서버에 API를 요청하는 부분은 일반적으로 위와 같은 형태이다.</p>
<p>만약 저게 어떠한 폼을 등록하는 요청이고, validation 에서 예외가 발생해도 200 에러 리스폰스를 주게 되면 당연히 응답값은 <code>handleSuccess()</code> 를 타게 될 것이다.</p>
<p>클라이언트 입장에서는 성공했다고 200을 받았는데, 나중에 자세히 확인해보니 아니었다는 상황이 발생하는 것이다.</p>
<p>POST 메서드는 처리 결과를 리스폰스로 주는 것이 이상적인 방법인 것을 알지만, 보통 등록 API 는 호출 후 <code>handleFailure()</code> 를 타는게 아닌 이상 리스폰스를 까서 확인하는 절차는 잘 구현돼있지 않다...</p>
<hr>
<h4 id="feign에서의-처리">Feign에서의 처리</h4>
<p><code>org.springframework.http.HttpStatus</code> 에는 다음과 같은 메서드가 정의되어있다.</p>
<pre><code class="language-java">/**
 * Whether this status code is in the HTTP series
 * {@link org.springframework.http.HttpStatus.Series#CLIENT_ERROR} or
 * {@link org.springframework.http.HttpStatus.Series#SERVER_ERROR}.
 * This is a shortcut for checking the value of {@link #series()}.
 * @since 5.0
 * @see #is4xxClientError()
 * @see #is5xxServerError()
 */
public boolean isError() {
  return (is4xxClientError() || is5xxServerError());
}
</code></pre>
<p>비슷하게, <code>FeignException</code> 은 <code>FeignClientException(4xx)</code> 과 <code>FeignServerException(5xx)</code> 를 전부 처리한다.</p>
<p>때문에 feign 등 서버간 통신에서 받는 경우, 200 으로 에러를 줘버리면 요청이 실패한 경우에도 무조건 response body 를 까봐야지 실패/성공을 구분할 수 있다.</p>
<p>또한 feign 을 통해 받아온 값을 캐싱할 때, 200 에러를 내려줘버리면 서버는 에러 응답값을 캐싱하게 된다. 
이는 일시적 서버 장애로 인한 에러여도, 캐시 만료 시간까지 해당 요청값에 대한 에러 상태가 유지될 수 있다는 의미이다.</p>
<hr>
<h4 id="그것이-약속이다">그것이 약속이다</h4>
<p>HTTP 상태코드는 HTTP 요청에 대한 서버의 응답 결과를 나타내는 표준 규약이다.
그런데 멋대로 커스텀 응답 코드를 표준으로 사용하면 기존의 표준 상태 코드 체계를 벗어나게 되고, 이는 협업시 어려움을 준다.</p>
<p>사실 이것만으로도 200 에러를 쓰면 안된다고 생각한다.</p>
<hr>
<h4 id="뭐가-정답인지는-모르겠다">뭐가 정답인지는 모르겠다</h4>
<p>반대로 에러를 그대로 던지는 것이 아니라 재처리를 해야 한다면, 200 에러를 주는 것이 편하다고 느낄 수도 있다.</p>
<p>feign의 경우 ErrorDecoder 을 커스텀해야 특정 에러에 대한 재처리를 구현할 수 있다.
(재처리는 retry 가 아닌 empty 처리 혹은 실패시 처리 동선을 타는 것을 말한다)</p>
<p>혹은 뭐 처리과정에서 문제가 있긴 있었는데 일단 처리됐다... 같은 결과면 200 예외와 400~500 중에 고민이 될수도 있겠다.</p>
<p>이런 것들이 그럼에도 불구하고 언쟁이 있는 이유가 아닐까?</p>
<blockquote>
<p><a href="https://stackoverflow.com/questions/27921537/returning-http-200-ok-with-error-within-response-body">Returning http 200 OK with error within response body</a>
<img src="https://velog.velcdn.com/images/_koiil/post/e1571db1-9009-4cef-bb5f-49475c968a79/image.png" alt=""></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[GC 튜닝의 의미와 여정]]></title>
            <link>https://velog.io/@_koiil/GC-%ED%8A%9C%EB%8B%9D</link>
            <guid>https://velog.io/@_koiil/GC-%ED%8A%9C%EB%8B%9D</guid>
            <pubDate>Tue, 21 Mar 2023 04:18:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://velog.io/@_koiil/GC">GC(Garbage Collectors)</a> 에서 이어지는 글입니다.</p>
</blockquote>
<h3 id="🚨-issue"><code>🚨 Issue</code></h3>
<p>성능 테스트 중 CPU 가 90~100까지 튀어서 테스트를 진행하기 어려운 상황이 되었습니다 🤦‍♂️
아직 목표 성능에 도달하지 못했기에... 원인이 무엇인지 분석하기 시작했습니다.</p>
<p>사실 가장 큰 원인은 대량의 데이터를 쿼리할 때 cpu 사용량이 오르는 것이었는데, 예상 가능한 내용이므로 품질 개선과 성능,안정성을 위해 다른 부분에서 작업을 진행하기로 했습니다.</p>
<p>GC 도 작업 대상 중 하나로 선정하여, GC로 인한 CPU 사용 빈도를 최소화하도록 조치했습니다.</p>
<p><em><strong>GC 빈도를 줄이기 위해 체크한 내용은 다음과 같습니다</strong></em></p>
<ul>
<li>GC 발생 빈도와 시간</li>
<li>메모리 누수가 있는지</li>
<li>new 영역이 너무 빨리 차고있지는 않는지</li>
<li>너무 크기가 커서 바로 old 로 가는 데이터가 있는지</li>
</ul>
<blockquote>
<p><code>Memory Leak 과 CPU</code>
메모리 누수가 발생하면, 사용하지 않는 객체 또는 메모리를 해제하지 않은 객체가 계속 쌓이기 때문에 시스템의 메모리 사용량이 계속 증가하게 됩니다.
메모리 사용량이 증가하면 시스템의 가용 메모리가 부족해지고, 최종적으로 OOM 으로 이어질 수 있습니다.
OS는 메모리 부족 상황을 해결하기 위해 메모리 압축(compression)이나 스왑(swap) 등으로 메모리를 재조정하고, 이 과정에서 CPU 사용량이 증가할 수 있습니다.
또한, Memory leak으로 인해 GC가 더 자주 발생할 수 있습니다. GC 에서 사용하지 않는 객체를 식별하고 메모리에서 제거하는 작업을 수행할 때 CPU 를 사용하기 때문에, 메모리 누수로 인해 GC의 빈도가 늘어나면 CPU 사용량이 증가할 수 있습니다.</p>
</blockquote>
<hr>
<h3 id="gc-튜닝"><code>GC 튜닝</code></h3>
<p><code>GC 를 튜닝한다</code> 는 말은 일반적으로 Full GC 빈도와 실행 시간을 줄이는 것을 의미합니다.</p>
<h4 id="적절한-힙-크기-설정">적절한 힙 크기 설정</h4>
<p>힙 크기를 적절하게 설정하여 Full GC 발생 빈도를 줄일 수 있습니다. 
너무 작은 힙 크기는 <code>OutOfMemoryError</code>를 발생시키고, 너무 큰 힙 크기는 GC 작업에 더 많은 시간이 소요되어 Full GC 발생 빈도가 늘어날 수 있습니다.
힙 메모리를 조정해가며 메모리 대비 GC 발생 빈도가 적당한 설정을 찾는 것이 중요합니다.
<img src="https://velog.velcdn.com/images/_koiil/post/288ba3df-901a-4f7e-abf2-109fdf8c101e/image.png" alt=""></p>
<h4 id="객체-재사용--수명-연장">객체 재사용 / 수명 연장</h4>
<p>객체를 생성하는 비용은 상대적으로 높기 때문에, 객체를 최대한 재활용 하거나 객체의 수명을 연장하여 GC 작업을 줄일 수 있습니다. 객체 수명이 짧은 경우 메모리 공간이 빠르게 차게 되므로, 객체 수명을 연장하면 GC 작업의 빈도를 줄일 수 있습니다.</p>
<blockquote>
<p><code>Spring</code>에서 <code>@Bean</code>으로 등록하는 객체들은 애플리케이션에서 사용될 때까지 메모리에 유지되므로, 일종의 객체 캐싱이나 객체 재사용의 역할을 합니다. <code>Spring</code>은 내부적으로 객체 풀링(pooling)을 사용하여 객체를 관리하기도 합니다. 이렇게 객체 수명을 연장하여 재사용하면, 객체를 반복적으로 생성하고 소멸시키는 오버헤드를 줄일 수 있어서 메모리 사용을 최적화할 수 있습니다.</p>
</blockquote>
<h4 id="softreference--weakreference-사용"><code>SoftReference / WeakReference</code> 사용</h4>
<p><code>SoftReference</code> / <code>WeakReference</code> 는 GC 작업을 수행할 때 우선적으로 해제되기 때문에, 메모리 누수를 줄일 수 있습니다.</p>
<pre><code class="language-java">import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;

public class ImageCache {
    private Map&lt;String, SoftReference&lt;Image&gt;&gt; cache;

    public ImageCache() {
        cache = new HashMap&lt;&gt;();
    }

    public Image getImage(String filename) {
        SoftReference&lt;Image&gt; ref = cache.get(filename);
        if (ref != null) {
            Image image = ref.get();
            if (image != null) {
                return image;
            }
        }

        Image image = loadImage(filename);
        cache.put(filename, new SoftReference&lt;&gt;(image));
        return image;
    }

    private Image loadImage(String filename) {
        // load image from file system or network
    }
}
</code></pre>
<p>자바의 Reference 와 GC 의 관한 내용 중 또 하나가 <code>Inner Class 와 nested static class</code>인데, Inner Class 는 상위 객체를 강참조하고 있기 때문에 GC 의 대상이 되지 않습니다.
이는 이펙티브 자바에도 나오는 내용으로, inner class 를 생성할 때는 static 으로 생성하는 것이 권장됩니다.</p>
<pre><code class="language-java">public class BaseClass {
    private Pointer first;

    private class InnerClass {
        // BaseClass 와 참조가 이어진 상태로, GC 대상에서 제외됨
    }

    private static class InnerClass {
        // heap memory 에 할당되어 GC 대상이 됨
    }
}</code></pre>
<p><a href="https://stackoverflow.com/questions/20380600/gc-performance-hit-for-inner-class-vs-static-nested-class">GC performance hit for inner class vs. static nested class</a>
<a href="https://d2.naver.com/helloworld/329631">Java Reference와 GC</a></p>
<h4 id="gc-변경"><code>GC</code> 변경</h4>
<p>GC 알고리즘을 조정하여 Full GC 작업을 최소화할 수 있습니다. GC 알고리즘은 애플리케이션의 메모리 사용 패턴에 따라 다르게 동작하므로, 적절한 알고리즘을 선택하고 튜닝하는 것이 중요합니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/a72d3fc2-2bb2-4799-904e-845f7548d0c5/image.png" alt=""></p>
<p>각 서비스의 WAS에서 생성하는 객체의 크기와 생존 주기가 모두 다르고, 장비의 종류도 다양합니다.
때문에 WAS의 스레드 개수와 장비당 인스턴스 개수, GC 옵션 등은 지속적인 튜닝과 모니터링을 통해서 해당 서비스에 가장 적합한 값을 찾아야 합니다.</p>
<h3 id="conclude"><code>Conclude.</code></h3>
<p>1차적으로 Heap 사이즈를 늘려 FullGC 발생 빈도를 줄였습니다.
그리고 nGrinder, VirtualVM, jstat 을 통해 GC 별 성능을 비교했고, 결과는 아래와 같았습니다.
해당 데이터를 기반으로 parallel -&gt; G1GC 로 변경하며 작업을 종료했습니다.</p>
<table>
<thead>
<tr>
<th align="center">GC</th>
<th align="center">GC 횟수</th>
<th align="center">총 GC 수행시간</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><code>parallel gc</code></td>
<td align="center">912-955</td>
<td align="center">34~39 sec</td>
</tr>
<tr>
<td align="center"><code>g1 gc</code></td>
<td align="center">435</td>
<td align="center">21 sec</td>
</tr>
<tr>
<td align="center"><code>g1 gc</code><br>(NewRatio 1)</td>
<td align="center">518</td>
<td align="center">23 sec</td>
</tr>
<tr>
<td align="center"><code>g1 gc</code><br>(NewSize=1536m MaxNewSize=1536m)</td>
<td align="center">341</td>
<td align="center">19 sec</td>
</tr>
<tr>
<td align="center"><code>cms gc</code></td>
<td align="center">3843</td>
<td align="center">2 min 23 sec(FullGC 발생)</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>참고</strong>
<code>-XX:MaxGcPauseMillis</code> 설정을 할 경우 G1GC 는 일시 중지 목표 시간을 충족하기 위해 Young 영역을 임의로 수정하기 때문에 <code>-Xmn</code>이나 <code>-XX:NewRatio</code> 등 Young Gen 사이즈를 명시적으로 세팅하는 것은 피해야 합니다.</p>
</blockquote>
<h3 id="ref"><code>Ref.</code></h3>
<p><a href="https://d2.naver.com/helloworld/37111">Garbage Collection 튜닝</a></p>
<h4 id="appendix-gc-모니터링"><code>Appendix: GC 모니터링</code></h4>
<table>
<thead>
<tr>
<th>방법</th>
<th>종류</th>
</tr>
</thead>
<tbody><tr>
<td>CUI</td>
<td>- <code>jstat</code> <br> <code>-verbosegc</code>  옵션</td>
</tr>
<tr>
<td>GUI</td>
<td>- <code>jconsole</code> <br>  - <code>Visual VM</code> <br>  - <code>Visual GC</code></td>
</tr>
</tbody></table>
<h4 id="appendix-간단한-oom-재현-방법"><code>Appendix: 간단한 OOM 재현 방법</code></h4>
<p><strong><code>OOME - Java Heap Space</code></strong>
Java Heap Space 에러는 가장 자주 일어나는 에러로, 기본적인 메모리 부족으로 인한 에러입니다.
이 에러는 단순히 생성하고자 하는 오브젝트가 JVM의 Heap 메모리 가용 영역을 넘어설 경우 발생시킬 수 있습니다.</p>
<pre><code class="language-java">public class Memory {
    public static void main(String[] args) throws Exception {
        int[] i = new int [10000*10000];
     }
}</code></pre>
<p>위 코드를 <code>–Xmx256m</code> 옵션과 함께 실행하면 Heapspace가 부족하다는 에러가 발생합니다.</p>
<p><strong><code>OOME 케이스 - GC Overhead Limit Exceeded</code></strong>
GC(Garbage Collector)가 너무 빈번하게 일어나서 오버헤드가 걸렸다는 뜻인데, GC가 동작하는 조건이 가용 메모리가 부족한 것으로부터 시작하기 때문에 근본적으로는 앞서 말했던 Heap 메모리 부족으로부터 시작한다고 볼 수 있습니다.</p>
<p>정확히는 GC 작업을 하느라 전체 동작시간의 98%를 소비했는데도 불구하고, Heap 메모리를 2% 이하로 확보했을 경우 이 에러가 발생합니다.</p>
<pre><code class="language-java">import java.util.*;

public class Memory {
    public static void main(String[] args) {
       Map map = System.getProperties();
       Random r = new Random();
       while (true) {
          map.put(r.nextInt(), &quot;value&quot;);
       }
    }
}</code></pre>
<p>해당 작업을 <code>–Xmx100m -XX:+UseParallelGC</code> 옵션과 함께 실행하면 에러를 재현할 수 있다.</p>
<p>에러 메세지는 GC 오류이지만 거의 대부분 Heapspace가 실제로 부족하거나, 큰 메모리를 사용하게 되는 코드가 있거나, 메모리 누수를 유발하는 코드가 어딘가에 있다고 보면 되기 때문에 괜히 GC라는 문구를 보고 튜닝을 시도해서는 안됩니다.</p>
<p><strong><code>OOME 케이스 - Metaspace</code></strong>
Metaspace는 Java의 Classloader가 현재까지 로드한 Class들의 메타데이터가 저장되는 공간입니다.
Java 계열의 언어에서 이름이 다른 Anonymous Class를 다량 생성하거나, 실제로 Class가 많은데 메모리가 부족할 경우에 해당 에러가 발생합니다.</p>
<p>일반적으로 Class를 무한정 생성하는 경우는 많이 없기 때문에 메모리 할당량을 늘려주는 것으로 해결되지만, 간혹 3rd Party Lib들이 Class들을 양산하고 있을 수 있습니다.</p>
<blockquote>
<p>주로 Scala, Kotlin 등이 제공하는 Command Line Compiler, REPL(Read Eval Print Loop)를 내부적으로 활용하거나, 이에 준하는 Janino같은 Runtime Compiler 또는 ScriptEngine을 사용한 어플리케이션, Javassist와 같은 Dynamic Class Generation을 활용한 어플리케이션을 긴 시간 동안 서비스할 때 많이 발생합니다.</p>
</blockquote>
<pre><code class="language-java">public class Memory {
    static javassist.ClassPool cp = javassist.ClassPool.getDefault();

    public static void main(String[] args) throws Exception {
       for (int i=0;; i++) {
         Class c = cp.makeClass(&quot;Generated&quot; + i).toClass();
       }
    }
}</code></pre>
<p>외부 라이브러리인 Javassist를 사용하고, <code>-XX:MaxMetaspaceSize=256m</code> 옵션으로 실행하면 Metaspace 에러를 발생시킬 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/359967ec-c3c4-4dd0-a8d5-7e4340b6a7af/image.png" alt=""></p>
<p><a href="https://www.samsungsds.com/kr/insights/1232761_4627.html">JVM 메모리의 이해와 케이스 스터디</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ehcache 가 메모리에서 사라지지 않는다]]></title>
            <link>https://velog.io/@_koiil/ehcache</link>
            <guid>https://velog.io/@_koiil/ehcache</guid>
            <pubDate>Wed, 15 Mar 2023 05:12:47 GMT</pubDate>
            <description><![CDATA[<h3 id="issue"><code>Issue</code></h3>
<p>서버가 CPU를 40% 이상 지속적으로 사용하고 있었습니다. 별다른 일을 하지는 않는데 왜일까요?
<img src="https://velog.velcdn.com/images/_koiil/post/8c5cd923-e3b0-4735-b03d-54cd706583b5/image.png" alt=""></p>
<h3 id="원인">원인</h3>
<p>full gc가 짧은 주기로 실행되고 있으며, gc 이후에도 메모리 해제가 거의 되고 있지 않고 있었습니다.
<img src="https://velog.velcdn.com/images/_koiil/post/371cfd6e-dedb-4d2d-96a0-bf2645ddcff1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/096e95b1-c84c-44ad-ad5d-8b1b58a643cc/image.png" alt=""></p>
<p>덤프를 떠서 확인해보니 이미 expire 된 ehcache element 들이 gc 이후에도 메모리에 존재하고있습니다.
왜 gc 대상 되지 않고 계속 살아있는 걸까요...🥲</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/f828877f-6a13-4b78-9bab-f9dada0b81d4/image.png" alt=""></p>
<p><a href="https://www.ehcache.org/documentation/2.8/configuration/data-life.html">Ehcache : Pinning, Expiration, and Eviction</a></p>
<p>ehcache 는 기본적으로 캐시 성능을 위해, expire 된 element를 바로 지우지 않습니다.
실제 gc 대상이 되려면 evict가 되어야 하는데, evict 트리거는 캐시 용량이 full 이어서 더 이상 캐시에 데이터 저장을 하지 못할 때, 미리 설정한 evict 알고리즘 (LRU ,LFU 등)에 의해 evict가 실행됩니다.
이 때, 비로소 해당 element의 객체가 unreachable 상태가 되어 gc 대상이 됩니다.</p>
<p>++ <a href="https://stackoverflow.com/questions/8838039/ehcache-does-not-remove-element-from-memory-on-eviction">ehcache does not remove Element from memory on eviction</a></p>
<blockquote>
<p>In Ehcache, expired cache elements are not immediately removed from memory. This is because Ehcache uses a passive expiration strategy, which means that it relies on the application to access or remove the expired elements.
When an element in the cache expires, it becomes eligible for eviction, but it remains in memory until it is either accessed or explicitly removed. This is because Ehcache does not actively check for expired elements or remove them from memory.
Therefore, if your application does not access or remove expired elements, they will continue to occupy memory until the cache reaches its maximum size and eviction occurs.
To avoid this situation, you can use a more aggressive expiration strategy, such as time-to-live (TTL) or time-to-idle (TTI), which actively check for expired elements and remove them from the cache. Alternatively, you can manually remove expired elements from the cache using the remove method.
It&#39;s also worth noting that Ehcache provides a feature called &quot;on-heap caching&quot; where the cache data can be stored directly on the Java heap. In this case, the expired cache elements will be removed during the next GC cycle of the JVM, which might not happen immediately. So, if you need to free up memory immediately, you can use the remove method or consider using off-heap or disk storage for your cache</p>
</blockquote>
<h3 id="solution"><code>Solution</code></h3>
<p>일단 로컬 캐시의 Max 설정을 줄여 full gc 가 발생하지 않게 했습니다.
또한 expired 된 캐시는 다시 호출될 때도 evict 처리되는데 남아있다는 것은 히트율이 떨어지는 것이라 판단해 캐시 구조도 변경했습니다!</p>
<p>(IMHO)
이번 일을 겪으니 왜 로컬캐시로 caffeine 대신 ehcache 를 쓰는지 더 알 수 없어졌습니다🤔
캐시 클러스터링을 하는 것도 아니고 off-heap을 쓰는 것도 아니고... 테스트했을 때 성능도 caffeine 이 더 좋게 나왔는데 왜 ehcache 를 쓸까요?
흠믐므...내일 팀장님한테 물어봐야겠습니다</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/5c6df637-e96b-4db5-a733-05a73047b6a3/image.png" alt=""></p>
<p><a href="https://xxun.tistory.com/252">ehcache does not remove Element from memory on eviction</a></p>
<hr>
<p>물어본 결과...
프로젝트 개발 당시에는 caffeine 이 Spring 지원 캐시가 아니었고, ehcache 는 스프링 지원 캐시여서 Ehcache 를 썻다고 합니다. 
하지만 현재는 Caffeine 도 스프링에서 지원하고 있고, 외에도 몇가지 이유가 있었지만 전부 Caffeine에서도 지원하고 있는 기능인 것으로 확인해 변경하기로 했습니다!</p>
<p>그리고 결론은 선택적으로 Ehcache 또는 Caffeine 를 적용할 수 있는 CacheManager을 만들게 되었습니다...
만드는건 좋긴 한데 물흐르듯 일감이 늘었네요🙄</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GC(Garbage Collectors)]]></title>
            <link>https://velog.io/@_koiil/GC</link>
            <guid>https://velog.io/@_koiil/GC</guid>
            <pubDate>Tue, 21 Feb 2023 03:57:31 GMT</pubDate>
            <description><![CDATA[<h3 id="gc-란"><code>GC 란?</code></h3>
<hr>
<p><code>GC</code>는 Java 프로세스가 한정된 메모리를 효율적으로 사용할 수 있게 하는 JVM 의 메모리 관리 작업입니다.
JVM은 자동으로 힙 메모리에서 사용하지 않는 객체를 식별, 수집, 해제하여, 프로그래머가 직접 메모리를 관리하는 부담을 줄여줍니다.</p>
<h4 id="java-heap"><code>Java Heap</code></h4>
<p><img src="https://velog.velcdn.com/images/_koiil/post/3f040aa8-bc18-491c-96fe-8cc2aa7b5b3f/image.png" alt=""></p>
<p>위 그림은 Jdk 7 이전의 Heap 메모리 구조입니다.</p>
<ul>
<li><code>Eden</code>: 새로 생성한 대부분의 객체가 위치하는 곳</li>
<li><code>S0, S1</code>: Eden 영역에서 GC가 한번 발생한 후 살아남은 객체들이 존재하는 곳</li>
<li><code>Old</code>: Young Generation에 대한 GC가 반복되는 과정속에 Tenuring Threshold 만큼 살아남은 객체가 존재하는 곳</li>
<li><code>Perm</code>: Class/Method 의 Meta 정보, static 변수/상수, JVM/JIT 관련 데이터 등들이 저장되는 곳</li>
</ul>
<blockquote>
<p><code>Java 8 이후로 permanent가 Native 영역인 Metaspace 로 변경되었고, 해당 영역에 있던 정보 중 static 변수/상수는 heap 영역으로 옮겨지면서 GC 대상에 포함되게 되었습니다.</code>
<img src="https://velog.velcdn.com/images/_koiil/post/b9fb97ba-addb-429b-9cff-55c8c17714f5/image.png" alt=""></p>
</blockquote>
<h4 id="minor-gc-full-gc"><code>Minor GC, Full GC</code></h4>
<p><img src="https://velog.velcdn.com/images/_koiil/post/2c7efdf4-553c-4b71-af1d-4e2ee6600a7e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/bf29ddf5-840e-4172-a18c-3e86b6753aba/image.png" alt=""></p>
<p><code>GC</code>가 실행될 때, JVM은 프로세스의 모든 스레드를 일시 중지(stop the world) 하고 사용하지 않는 객체를 식별하고 수집합니다.
Full GC의 실행 시간은 상대적으로 Minor GC에 비하여 길기 때문에, GC 실행에 <span style="color:red"><code>STW</code></span> 가 1초 이상이 소요되게 되면 연계된 여러 부분에서 타임아웃이 발생할 수 있습니다. </p>
<p>때문에 JVM은 <span style="color:red"><code>STW</code></span> 시간을 최소화하고 성능을 향상시키기 위해 다양한 <code>GC 알고리즘</code>을 제공하고 있고,  적절한 <code>GC 알고리즘</code>을 선택하고 힙 크기를 조정하여 애플리케이션의 성능을 최적화할 수 있습니다.</p>
<p>(허나 대부분의 상황에서 <code>GC 튜닝</code>까지는 필요하지 않다는 것을 유의해야 합니다.)</p>
<h3 id="gc-알고리즘"><code>GC 알고리즘</code></h3>
<hr>
<h4 id="serial-gc--xxuseserialgc"><code>Serial GC (-XX:+UseSerialGC)</code></h4>
<p><code>Java 5,6</code>의 default GC 로 싱글 스레드로 동작하기 때문에 STW가 다른 GC에 비해 오래걸립니다. <code>Serial GC</code>는 적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식이며, <code>mark &amp; sweep &amp; compact</code> 알고리즘을 사용합니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/1c36a301-9fc0-4e94-acc1-571b4551218f/image.png" alt=""></p>
<ul>
<li><code>Mark</code>는 Old 영역에 살아있는 객체, 즉 gc 대상이 아닌 객체에 대해 식별하는 역할을 합니다.</li>
<li><code>Sweep</code>은 Heap의 앞 부분부터 mark 된 Object를 제외하고 제거합니다.</li>
<li><code>Compact</code>는 Sweep 이후 비어있는 Heap 공간들을 연속되게 쌓이도록 힙의 앞 부분부터 채우는 과정입니다.</li>
</ul>
<h4 id="parallel-gc--xxuseparallelgc"><code>Parallel GC (-XX:+UseParallelGC)</code></h4>
<p><code>Java 8</code>의 default GC로, <code>Serial GC</code>와 기본적인 알고리즘은 같지만 <code>Serial GC</code> 는 단일 스레드인데에 비해 <code>Parallel GC</code>의 Minor GC는 멀티 스레드로 동작합니다.(Full GC는 동일)
<code>Parallel GC</code>는 메모리가 충분하고 코어의 개수가 많을 때 유리하며, Throughput GC라고도 부릅니다.</p>
<h4 id="parallel-old-gc--xxuseparalleloldgc"><code>Parallel Old GC (-XX:+UseParallelOldGC)</code></h4>
<p><code>Parallele GC</code>를 업그레이드한 버전으로, Full GC 또한 병렬적으로 처리하며 <code>mark &amp; summary &amp; compact</code> 알고리즘을 사용합니다.
Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아 있는 객체를 식별한다는 점에서 차이가 있습니다.</p>
<h4 id="cms-gc--xxuseconcmarksweepgc"><code>CMS GC (-XX:+UseConcMarkSweepGC)</code></h4>
<p><code>CMS GC</code>는 Full GC의 수행시간을 최소한으로 하는데 초점을 둔 GC 방식으로, <span style="color:red"><code>STW</code></span> 의 시간을 최소화 합니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/cad14d4b-ffc7-48ff-a18a-f8ddac17da23/image.png" alt=""></p>
<ul>
<li><code>Initial Mark</code> : <span style="color:red"><code>STW</code></span> 현재 살아남은 객체를 탐색하는데, 클래스 로더에서 가장 가까운 객체(GC ROOT에서 참조하는 객체)들만 우선적으로 탐색하기 때문에 STW발생 시간이 매우 짧습니다.</li>
<li><code>Concurrent Mark</code> : Initial Mark에서 탐색한 객체들이 참조하고 있는 객체를 찾아가며, GC의 대상인지 판별합니다.</li>
<li><code>ReMark</code> : <span style="color:red"><code>STW</code></span>  Concurrent Mark 과정 중 새로 생성된 객체나, 참조자 끊기는 등 변경된 객체가 있는지 다시한번 검사합니다.  (멀티스레드로 동작하기 때문에 STW 시간이 짧음)</li>
<li><code>Concurrent Sweep</code> : STW 없이 Remark 단계까지 검증이 완료된 GC대상 객체들을 삭제합니다.</li>
</ul>
<p>하지만 <code>CMS GC</code>는 다른 GC 방식보다 메모리와 CPU를 더 많이 사용하고, Compaction 작업을 기본적으로 진행하지 않기 때문에 메모리 단편화에 대한 문제를 신경써야 합니다.</p>
<p>만약 연속적인 메모리 할당이 불가능할 정도로 메모리 단편화가 진행되었다면 Compaction 작업을 수행해야 하는데, 해당 작업은 다른 GC에서의 Compaction작업에 비해 STW 시간이 길어집니다.
따라서 <code>CMS GC</code>를 사용할때는 Compaction 작업이 얼마나 자주, 오래 실행되는지를 검토 후에 사용해야합니다.</p>
<blockquote>
<p><a href="https://blog.gceasy.io/2019/02/18/cms-deprecated-next-steps/#more-2947">Java 9 부터 deprecated 되었고</a> <a href="https://openjdk.org/jeps/363">Java 14 에서는 중지되었습니다.</a></p>
</blockquote>
<h4 id="g1garbage-first-gc"><code>G1(Garbage First) GC</code></h4>
<p><code>Java 9</code> 의 default GC로, G1 GC는 장기적으로 문제가 야기될 가능성이 있는 <code>CMS GC</code>의 대체 방안으로 고안되었으며, 성능상 뛰어나다는 장점이 있습니다.</p>
<p><code>G1 GC</code>는 메모리를 바둑판처럼 각각의 영역으로 구분하고 각 영역에 객체를 할당하여 GC를 실행합니다.
그러다가, 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행합니다.
즉 기존의 Young, Old 영역 대신 힙 메모리 영역 자체를 Region 이라는 논리적인 단위로 나눠서 관리하며, 이렇게 나뉜 Region에 특정 역할(Eden,Survivor,Old 등)을 동적으로 부여하는 방식입니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/b6fcdd94-c855-4768-a24b-71b3cf1d5c76/image.png" alt=""></p>
<h4 id="zgc--xxusezgc"><code>ZGC (-XX:+UseZGC)</code></h4>
<p><a href="https://wiki.openjdk.org/display/zgc/Main">ZGC</a> 는 JDK 15에서 기준 Production Ready 상태로, 조금 더 큰 메모리(8MB ~ 16TB) 에서 효율적으로 Garbage Collect 하기 위한 알고리즘입니다.</p>
<blockquote>
<p>ZGC : a good fit for server applications, where large heaps are common, and fast application response times are a requirement.</p>
</blockquote>
<p>ZGC는 STW 시간을 최대한 적게(10ms 이하로) 가져가기 위해, Mark 의 시작과 끝, 재배치에만 STW가 발생합니다.</p>
<ul>
<li><code>Mark Start</code> : <span style="color:red"><code>STW</code></span> ZGC의 Root에서 가리키는 객체 Mark 표시</li>
<li><code>Concurrent Mark/Remap</code> : 객체의 참조를 탐색하면서 모든 객체에 Mark 표시</li>
<li><code>Mark End</code> : <span style="color:red"><code>STW</code></span> 새롭게 들어온 객체들에 대해 Mark 표시</li>
<li><code>Concurrent Pereare for Relocate</code> : 재배치하려는 영역을 찾아 Relocation Set에 배치</li>
<li><code>Relocate Start</code> : <span style="color:red"><code>STW</code></span> 모든 Root 참조의 재배치를 진행하고 업데이트</li>
<li><code>Concurrent Relocate</code> : 이후 Load Barriers 를 사용하여 모든 객체를 재배치 및 참조 수정</li>
</ul>
<blockquote>
<p>ZGC doesn&#39;t get rid of stop-the-world pauses completely.
The collector needs pauses when starting marking, ending marking and starting relocation.
But this pauses are usually quite short - only a few milliseconds.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/_koiil/post/b2c69728-e404-431a-add5-60ccf234c127/image.png" alt=""></p>
<p>G1 GC 와 비슷하게 ZGC 또한 힙 영역을 리전으로 분류합니다. 각 리전 타입에 따라 저장될 수 있는 객체의 크기는 아래와 같습니다.
만약 6M 짜리 객체가 들어온다면, 4M 이상이므로 Large Page에 할당되게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/485b6d83-a1fc-4bf6-9355-760dd6cce1b9/image.png" alt=""></p>
<p><code>ZGC</code>는 GC 메타데이터를 객체의 메모리 주소에 표시합니다(<a href="https://www.oreilly.com/library/view/java-11-and/9781789133271/99bc8b28-9595-4065-aec8-5eaddddbdda4.xhtml">Colored Pointers</a>).
<code>ZGC</code>는 64비트만 지원하는데, 메모리의 주소 파트로 42비트(4TB)를 사용하고 다른 4비트를 하기 GC metadata 를 저장하는 용도로 사용합니다.
<img src="https://velog.velcdn.com/images/_koiil/post/1e3ba9e4-89e9-495a-b5c6-e9c7a582112b/image.png" alt=""></p>
<ul>
<li><code>Finalizable</code>: finalizer를 통해서만 참조되는 Objectd의 Garbage</li>
<li><code>Remapped</code>: 재배치 여부를 판단하는 마크</li>
<li><code>Marked 1(or 0)</code>: Live Object </li>
</ul>
<p><code>ZGC</code> 관련 내용은 아래 아티클에 잘 나와있습니다.
<a href="https://www.blog-dreamus.com/post/zgc%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C">ZGC에 대해서</a>
<a href="https://hub.packtpub.com/getting-started-with-z-garbage-collectorzgc-in-java-11-tutorial/">Getting started with Z Garbage Collector (ZGC) in Java 11 [Tutorial]</a>
<a href="https://www.sobyte.net/post/2022-01/notes-zgc/">ZGC Notes: Colored Pointers</a></p>
<hr>
<h2 id="ref"><code>Ref.</code></h2>
<p><a href="https://www.baeldung.com/jvm-garbage-collectors">JVM Garbage Collectors</a>
<a href="https://tschatzl.github.io/2022/03/14/jdk18-g1-parallel-gc-changes.html">JDK 18 G1/Parallel/Serial GC changes</a>
<a href="https://catsbi.oopy.io/56acd9f4-4331-4887-8bc3-e3e50b2f3ea5">Garbage Collection</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 127과 128]]></title>
            <link>https://velog.io/@_koiil/127%EA%B3%BC-128</link>
            <guid>https://velog.io/@_koiil/127%EA%B3%BC-128</guid>
            <pubDate>Mon, 20 Feb 2023 12:28:58 GMT</pubDate>
            <description><![CDATA[<p><code>==(동등연산자)</code>와 <code>equals</code> 에 대해서는 이전 <a href="https://velog.io/@_koiil/Java-%EB%9E%98%ED%8D%BC-%ED%81%B4%EB%9E%98%EC%8A%A4wrapper-class">Wrapper Class</a> 글에서도 잠시 다룬 적이 있습니다.</p>
<p><code>== 연산자</code>는 내부의 값을 비교하는 것이 아니라 객체의 레퍼런스 주소를 비교하기 때문에, Wrapper 객체의 값 비교로는 사용할 수 없고 <code>equals</code> 로 내부 값을 얻어 비교해야한다는 내용이었습니다.</p>
<blockquote>
<p><strong>동일성과 동등성</strong>
동일성 : 두 객체가 할당된 메모리 주소가 같으면 동일. <code>==</code> 를 통해 비교 가능
동등성 : 두 객체의 값이 같으면 동등. <code>equals</code> 를 통해 비교 가능
원시 타입은 객체가 아니기때문에 주소가 없으므로, <code>==</code> 연산자를 사용하였을 때 값이 같으면 동일하다고 나옵니다.</p>
</blockquote>
<p>더불어 작게 언급했던 내용이, Java는 -128 ~ 127 사이의 값은 상수풀에서 관리하기 때문에, 이 범위 내의 숫자들은 <code>==</code> 로 비교해도 true 가 나오게 된다는 내용입니다. (미리 정정하자면 상수풀이 아니라 IntegerCache)</p>
<pre><code class="language-java">Integer i1 = 128;
Integer i2 = 128; 
assertTrue(i1==i2); // false        
assertTrue(i1.equals(i2)); // true    

Integer i3 = 127;
Integer i4 = 127;        
assertTrue(i3==i4); // true        </code></pre>
<p>비슷한 내용으로 String 에서도 <code>==(동등연산자)</code>와 <code>equals</code> 에 관한 내용은 단골 주제였는데요.
아래와 같은 상황은 왜 발생하고, 어떤 이유때문에 이러한 구조가 되었는지 알아보게 되었습니다.</p>
<pre><code class="language-java">String str1 = &quot;koiil&quot;;
String str2 = &quot;koiil&quot;;
String str3 = new String(&quot;koiil&quot;);
String str4 = new String(&quot;koiil&quot;);


assertTrue(str1==str2); // true    
assertTrue(str1==str3); // false    
assertTrue(str3==str4); // false    

assertTrue(str1.equals(str2)); // true    
assertTrue(str1.equals(str3)); // true    
assertTrue(str3.equals(str4)); // true    </code></pre>
<hr>
<h2 id="상수풀">상수풀</h2>
<p><code>상수풀(Constant Pool)</code>은 Java의 heap 영역에 존재하는 공간으로, 상수들을 저장하는 용도로 사용됩니다.
상수에는 문자열 리터럴 뿐만 아니라, final로 선언된 원시타입 등도 포함될 수 있습니다.</p>
<ul>
<li><code>immutable</code> : 상수풀은 자바 클래스 파일의 일부분으로 컴파일 시 생성되므로, 런타임 시 상수풀에 저장된 값들은 수정이 불가능해 불변성을 보장합니다.</li>
<li><code>메모리 최적화</code> : 상수들은 자바 컴파일러에 의해 상수풀에 저장되고, 해당 상수가 필요한 코드에서는 상수풀에서 값을 참조하여 사용합니다.</li>
</ul>
<p><code>Java 7</code>까지는 상수풀의 위치가 PermGen 영역에 존재했습니다. Perm 영역은 보통 Class의 Meta 정보나 Method의 Meta 정보, Static 변수와 상수 정보들이 저장되는 공간으로 흔히 메타데이터 저장 영역이라고도 합니다.
하지만 PermGen 영역은 런타임에 변경할 수 없는 고정된 사이즈이기 때문에, intern 메서드를 과도하게 사용하면 저장할 공간이 부족해 OOM(Out Of Memory) 이 발생할 수도 있었습니다.</p>
<p>그래서 <code>Java 8</code>부터는  PermGen 영역은 완전히 사라지고, 상수풀은 Heap 영역으로, 메타데이터는 MetaSpace라는 새로운 네이티브 영역으로 옮겨지게 되었습니다.
Heap 영역으로 변경된 이후에는 상수풀도 GC의 대상이 되었고, 메타데이터는 Metaspace 영역에서 더 큰 메모리 영역을 가질 수 있게 되었습니다.</p>
<pre><code class="language-java">//Java 7 HotSpot JVM
&lt;----- Java Heap -----&gt;             &lt;--- Native Memory ---&gt;
+------+----+----+-----+-----------+--------+--------------+
| Eden | S0 | S1 | Old | Permanent | C Heap | Thread Stack |
+------+----+----+-----+-----------+--------+--------------+
                        &lt;---------&gt;
                       Permanent Heap
S0: Survivor 0
S1: Survivor 1

//Java 8 HotSpot JVM

&lt;----- Java Heap -----&gt; &lt;--------- Native Memory ---------&gt;
+------+----+----+-----+-----------+--------+--------------+
| Eden | S0 | S1 | Old | Metaspace | C Heap | Thread Stack |
+------+----+----+-----+-----------+--------+--------------+</code></pre>
<h3 id="string-constant-pool"><code>String Constant Pool</code></h3>
<p>Java에서 String을 생성하는 방법은 객체 생성과 리터럴이 있습니다.</p>
<pre><code class="language-java">String a = new String(&quot;a&quot;);   // String Object
String a = &quot;a&quot;;              // String Literal</code></pre>
<p>new 연산자를 통해 문자열 객체를 생성하는 경우 메모리의 <code>Heap</code> 영역에 할당되고,
리터럴을 이용해 생성하는 경우에는 <code>heap</code> 중에서도 <code>String Constant Pool</code> 영역에 할당됩니다.</p>
<p>클래스가 JVM에 로드되면 모든 리터럴이 상수풀에 위치하게 되는데, <code>String interning</code>은 이러한 문자열 리터럴들을 상수풀에 저장하는 것을 의미합니다.
<code>intern</code> 메서드는 _상수풀에 해당 문자열이 존재하면 상수풀에서 문자열을 가져오고, 존재하지 않으면 상수풀에 문자열을 추가하고 해당 문자열을 반환_합니다.
자바 컴파일 시 단순히 String 리터럴을 가져오기만 하는 것이 아니라, intern 메서드를 수행하여 상수풀에 추가하게 됩니다.</p>
<pre><code class="language-java">String constantString = &quot;koiil&quot;;
String newString = new String(&quot;koiil&quot;);

assertThat(constantString).isSameAs(newString);  // false

String internedString = newString.intern();

assertThat(constantString).isSameAs(internedString);  // true</code></pre>
<h3 id="integercache"><code>IntegerCache</code></h3>
<p>도입부에서 언급했듯이, Integer 에서 127과 128은 동일값을 == 비교했을 때의 결과가 다릅니다.
마치 상수풀에 저장된 문자열처럼 말이죠.</p>
<p>Integer, Long 등의 타입 또한 메모리 사용을 최적화하기 위해 같은 값을 갖는 객체나 상수를 중복해서 생성하지 않고, 기존 값을 참조하는 방법을 사용하고 있습니다.</p>
<p>int 리터럴을 Integer로 직접 대입하는 것은 auto-boxing 의 예입니다.
리터럴 값이 객체로 변환되는 코드는 컴파일러에 의해 수행되고, 컴파일 시간동안, 컴파일러는 <code>Integer a = 127;</code>을 <code>Integer a = Integer.valueOf(127);</code>로 변경합니다.
그리고 <code>Integer.valueOf()</code> 메서드를 살펴보면 <code>IntegerCache</code> 의 존재를 알 수 있습니다.</p>
<pre><code class="language-java">// Integer.java
@IntrinsicCandidate
public static Integer valueOf(int i) {
    if (i &gt;= IntegerCache.low &amp;&amp; i &lt;= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)]; //캐시 내 값이면 캐시 반환
    return new Integer(i);   // 외에는 새 객체를 생성
}

private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer[] cache;
        static Integer[] archivedCache;

        ...

        if (archivedCache == null || size &gt; archivedCache.length) {
            Integer[] c = new Integer[size];
            int j = low;
            for(int i = 0; i &lt; c.length; i++) {
                c[i] = new Integer(j++);
            }
            archivedCache = c;
        }
        ...
}</code></pre>
<p>처음 Integer 이 호출되는 순간, IntegerCahe 는 -128 ~ 127 사이의 값을 미리 생성해 캐시 배열에 저장해둡니다.
그리고 이후 범위 내의 값을 호출할 시, 캐시된 값을 돌려줍니다.</p>
<p>이러한 캐싱은 Integer 만 있는 것이 아니라 <code>ByteCache</code>, <code>ShortCache</code>, <code>LongCache</code>, <code>CharacterCache</code>도 각각 존재합니다.</p>
<h3 id="flyweight-패턴"><code>Flyweight 패턴</code></h3>
<p>이러한 디자인 패턴을 플라이웨이트(Flyweight) 패턴이라고 합니다.</p>
<p>플라이웨이트 패턴(Flyweight Pattern)은 객체 지향 디자인 패턴 중 하나로, 많은 수의 유사한 객체를 생성할 때 발생하는 메모리 사용량을 최소화하고 성능 저하 문제를 해결하기 위한 패턴입니다.</p>
<p>플라이웨이트 패턴은 객체를 공유 객체(Shared Object)와 비공유 객체(Unshared Object)로 분류하는데, 비공유 객체를 공유 객체로 대체해 객체 생성 횟수를 줄이고 메모리 사용량을 최적화합니다.</p>
<p>이 패턴은 객체의 내부 상태와 외부 상태를 분리해서, 내부 상태를 공유 객체로 관리하고, 외부 상태를 비공유 객체로 관리합니다.
즉, 객체의 <strong>공통적인</strong> 내부 상태를 공유해서 메모리 사용량을 줄이고, <strong>고유한</strong> 외부 상태를 비공유 객체로 처리해서 다양한 변화에 대응할 수 있도록 합니다.</p>
<h2 id="ref"><code>Ref.</code></h2>
<p><a href="https://www.baeldung.com/java-string-pool">Guide to Java String Pool</a>
<a href="https://meetup.nhncloud.com/posts/185">[Java] Integer.valueOf(127) == Integer.valueOf(127) 는 참일까요?</a>
<a href="https://johngrib.github.io/wiki/java8-why-permgen-removed/">JDK 8에서 Perm 영역은 왜 삭제됐을까</a>
<a href="https://cornswrold.tistory.com/265">인터닝(Interning)이란 무엇인가?</a>
<a href="https://www.nextree.co.kr/p11101/">동등비교와 정렬</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] 글또 8기를 시작하며]]></title>
            <link>https://velog.io/@_koiil/%EA%B8%80%EB%98%90-8%EA%B8%B0%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@_koiil/%EA%B8%80%EB%98%90-8%EA%B8%B0%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0</guid>
            <pubDate>Sun, 12 Feb 2023 14:31:54 GMT</pubDate>
            <description><![CDATA[<p>예전에 지인들의 레포에서 글또 오가니제이션을 접하게 되었고, 글또콘이나 커피드백 등을 진행하는 것을 보며 언젠간 꼭 글또를 해보고싶다고 생각했었다.</p>
<p>그러다 이번에 개발바닥1사로에서 글또 모집 중이라는 톡을 보고 내친김에 지원하게 되었다!!</p>
<p><a href="https://www.notion.so/zzsza/ac5b18a482fb4df497d4e8257ad4d516">👉 글쓰는 또라이가 세상을 바꾼다</a></p>
<p><img src="https://velog.velcdn.com/images/_koiil/post/4af4b21f-10a6-43bd-9e8d-dbd61f563132/image.png" alt=""></p>
<p>사실 지원할 때 쯤엔 벌여 놓은 일들이 너무 많아서 (넥터 운영진, 스터디들, 게임 개발, 신규 프로젝트 등등,,) 글또까지 과연 할 수 있을까? 싶었는데 뭐 지금은 어느정도 정리가 돼서, 첫 기수 활동인만큼 조금 불태워 보자는 마음이다🔥</p>
<hr>
<h2 id="6개월간의-글또를-시작하며">6개월간의 글또를 시작하며</h2>
<p>글또까지 참여한 김에, 블로그를 조금 더 의미있게 사용해보고자 한다!</p>
<h4 id="1-트러블슈팅과-설계-개발-과정을-더-많이-기록해두고싶다">1. 트러블슈팅과 설계, 개발 과정을 더 많이 기록해두고싶다.</h4>
<p>예전에는 남들은 엄청나게 딥다운한 경험, low level 까지 뜯어보며 분석한 것들을 포스팅하는데 나는 이론 정리와 단순 과정만 남겨두는 것이 부끄러웠다.
이런 것들을 공개적으로 써도 되는 것인지 정말 고민했었다🥲</p>
<p>하지만 돌아보면, 난 기억력이 좋지 않아서 늘 내 과거의 글을 참고한다.
포트폴리오에 쓸 아웃풋이 아니더라도, 나를 위한 기록으로 글을 쓰기로 했다.</p>
<h4 id="2-개발일기에-더-많은-글을-써보고자-한다">2. 개발일기에 더 많은 글을 써보고자 한다.</h4>
<p>일기장처럼 개인적인 생각과 경험을 작성하려고 만든 컬렉션인데, 최근에는 테마가 애매한 잡다한 글들을 방치하는 컬렉션으로 사용하고있었다.
작년도에는 새로운 것들을 엄청나게 많이 했었고, 그로 인해 자극을 받고 스스로 많이 변했기 때문인지 임시저장해둔 글들이 몇 편 생겼다.
다시 읽어보니까 그때 생각도 나고 다시 콩닥콩닥해지는게, 이런 것들도 잘 기록해두면 좋겠다 싶어 이번 기간에는 한편씩 남겨보고자 한다.</p>
<h4 id="3-위-글들을-읽기-편하게-쓸-수-있게-되고싶다">3. 위 글들을, 읽기 편하게 쓸 수 있게 되고싶다.</h4>
<p>내가 제일 좋아하는 책들은 늘 읽을 때 불편해서 한 구절마다 멈추게 되는 문장들이었다.
차라투스투라는 이렇게 말했다, 선악의 저편...등 철학, 중에서도 니체를 좋아했었다.</p>
<p>그 영향인지 글을 쓸때 꼭 꼬아서 쓰거나, 미사여구와 사담으로 앞 뒤에 살을 붙여 핵심을 숨기는 글을 자주 쓰게 되었다. 이런 습관들은....현재에 와서 정보전달 아티클을 쓸 때는 엄청난 약점이 되어버렸다.😅
(원래도 글재주가 있는 편이 아니기도 하지만)</p>
<p>저번에 어떤 블로그를 봤는데, 그 사람의 철학과 기술적인 내용이 정말 완벽하게 얼라인 되어있었다.
읽기도 편해서 그대로 블로그의 모든 글을 정주행해버렸다.
명확한 롤모델이 생기니 더 열정이 생기는 것 같다. 이번에는 잘 읽히는 글을 쓰도록 노력해보고싶다!</p>
<hr>
<p>21년도 회고를 쓰며 목표로 <strong>글 쓰는 개발자</strong>를 적어두었던 것 같은데, 상반기에는 좀 쓰다가 하반기에는 일에 치여 임시저장만 10편 적어둔 채로 마무리했다.
올해는 저 아가포스팅들이 차가운 임시저장함에서 나와 세상빛을 보도록...꼭...꼭 완성하고 말테다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] NEXTERS 21기 활동, 그리고 22기 CTO]]></title>
            <link>https://velog.io/@_koiil/NEXTERS-21%EA%B8%B0-%ED%99%9C%EB%8F%99-%EA%B7%B8%EB%A6%AC%EA%B3%A0-CTO</link>
            <guid>https://velog.io/@_koiil/NEXTERS-21%EA%B8%B0-%ED%99%9C%EB%8F%99-%EA%B7%B8%EB%A6%AC%EA%B3%A0-CTO</guid>
            <pubDate>Tue, 07 Feb 2023 05:39:19 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="회고-및-후기-포스팅이므로-개인적인-견해가-다수-포함되어있습니다-❗️">회고 및 후기 포스팅이므로 개인적인 견해가 다수 포함되어있습니다 ❗️</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/_koiil/post/9570181e-fcb4-4208-9f88-e0e7c96ba2e9/image.png" alt=""></p>
<h2 id="📌-nexters"><code>📌 Nexters</code></h2>
<p><a href="https://teamnexters.com/">넥스터즈</a>는 개발자와 디자이너가 모여 프로젝트를 진행하는 IT 동아리이다.
원래 사이드프로젝트를 즐겨했고, 네트워킹하는 것도 좋아해 이런 스터디나 동아리를 좋아했으나 이전 동아리에서 쓴맛을 겪어 한동안 멀리했다...
<span style="color:gray"> (3달동안 커밋 하나도 안하면서 벌금때매 회의는 들어오는 팀원... 얘기했더니 다음부터 우리팀 근처로는 안오던 운영진...^^) </span></p>
<p>그러다가 어느정도 여유가 생기고 사이드 프로젝트가 그리워질때 쯤, 매시업을 하는 지인의 얘기를 듣고 빅-동아리에 지원해보자!해서 넥스터즈에 지원하게 되었다.</p>
<p align="center" style="color:gray">
  <img style="margin:50px 0 10px 0" src="https://velog.velcdn.com/images/_koiil/post/c2b32af4-0cfb-4ff0-9cae-1ed7a0b58240/image.png" alt="factorio thumbnail" width=300 />
</p> 




<p>지원서 쓸 때는 취뽀한지 얼마 안돼서 직장인뽕 + 떨어져본적없음 콤보로 대충 지원하면 뽑히겠지~하고 쓱쓱 써서 냈는데 면접 때보니 네임드 회사 개발자들도 많고, n년차도 많아서 당황했다;;;
여담이지만 이후에 면접자들의 스펙과 개발자 TO를 확인해보니 보니 어케 붙었나 싶었다.
(면접 때 술먹고 놀기 좋아한다고 했는데 마침 해당 기수 컨셉이 네트워킹이라 뽑힌듯ㅎ)</p>
<hr>
<h2 id="🏃♂️-21기-활동"><code>🏃‍♂️ 21기 활동</code></h2>
<p>아이디어 선정부터 꽤 재밌어보이기도 하고, 평소에도 관심있던 주제가 있었고, PM이 <code>인간E</code>인 것 같아서 냅다 지원했다. (실제로 걍 대문자 E 미쳐버린 사람이었음..미치광E)
여러 집단에 속하며 느낀 거지만, 프로젝트나 목표를 향한 열정이나 집중력도 중요하지만 잘 놀고 쉴줄 알아야 커뮤니케이션에서도 서로 양보가 잘되고 협업 능률이 올라가는 듯 하다.</p>
<p>다행히 팀원들도 합이 좋은 사람들로 이루어졌고, 처음부터 피봇팅을 하기로 하고 들어와서 기획이 조금 길어지긴했으나 오랜만에 맘대로 하는 개발도 재미있었다.
역시 다른 사람이랑 일하다보니 재밌는 것도 많이 배우고 서로서로 자극이 되는게 너무 좋았다🔥</p>
<p align="center" style="color:gray">
  <img style="margin:50px 0 10px 0" src="https://velog.velcdn.com/images/_koiil/post/919aa809-eab7-41c8-93f0-b516599ec748/image.png" alt="factorio thumbnail" width=300 />
 (어벤져스같던 우리팀)
</p> 


<p>자율적으로 스터디도 많이 진행하고, 팀끼리 클라이밍이나 축제도 많이 다니는거보며 넥터는 정말 사람 좋고 열정 가득한 사람들만 모인 듯 했다.</p>
<p>일단 어영부영 넘어가는 운영 + 알아서 주도적으로 개발하세요~가 아니라 매주 커리큘럼이 있고, 완수를 위한 체계가 짜여있어서 확실이 오래되고 큰 동아리는 다르다 싶었다.
오프라인 세션이다보니 사람들과도 매주 놀고... <span style="color:gray">(회식러버 행복)</span> 매주 마실 나가는게 재밌었던 두 달이었다.</p>
<p>특히나 마감 전 주에는 넥나잇이라고 해커톤같은 행사가 있는데, 혼자 밤샘개발은 많이했어도 이렇게 다같이 달리는 개발은 처음이라 너무 재밌고 콩닥콩닥했다.
이후에 맛들려서 유니톤도 나갔었는데, 퇴근하고 해커톤 달리고 출근할라니까 죽겠더라...🧨</p>
<hr>
<h2 id="🙌-22기에는-운영진으로"><code>🙌 22기에는 운영진으로</code></h2>
<p>처음부터 CEO 자리를 탐내던 PM이 결국 22기 운영진으로 당선이 됐는데, 술자리에서 운영진라이팅 당해서 왠지 그 감투가 멋지고 좋아보였던 나는 결국 CTO를 맡게 되었다.
기존 사이드 프로젝트에서도 리드 개발을 맡았었고, 멘토링도 간간히 했어서 운영진도 한번 해볼까? 싶은 마음으로 시작했는데... 그런 것들이랑은 결이 완전히 달랐다😂</p>
<p>그리고 그리고 임기가 끝나고 인수인계까지 마무리 된 지금, 생각과는 다른 점이 많지만 하길 잘했다는 생각이 든다.
운영진으로 활동하며 기억에 남는 것 몇가지를 남겨보고자한다.</p>
<h3 id="🗓-리쿠르팅"><code>🗓 리쿠르팅</code></h3>
<p>서류 검토가 힘들다는 말은 익히 들었고, 타동아리 운영진하는 친구가 리쿠르팅 시즌마다 며칠 밤 새며 눈 빨갛게 돌아다니는 모습을 봤음에도 방심해버렸다.
서류 양은 상상 이상이었고 하나씩 상세하게 읽기에는 시간이 절대적으로 부족했다.
정말... 3줄 띡 써둔 서류를 보면 고마우면서 미운 감정이 교차했다....</p>
<p align="center" style="color:gray">
  <img style="margin:50px 0 10px 0" src="https://velog.velcdn.com/images/_koiil/post/8d4de83a-9be6-4b3d-b83e-a76fb6084dce/image.png" alt="factorio thumbnail" width=300 />
</p> 

<p>내가 면접관 입장에서 서류를 검토하다보니 1차적으로 걸러지는 서류들의 특징을 체감할 수 있었다.
뭐 3줄짜리 서류, ㅎㅎㅋㅋ 용용체(충격), 타인을 깎아내리는 내용, 검증되지 않는 글로만 작성하고 확인할 포폴하나 없는 지원서 등...</p>
<p>회사도 아닌 동아리 지원서에 공들이기 민망한 것도 알지만, 같이 지원하는 사람들 중 누군가에겐 절실하고 선망하는 곳일수도 있다는 것을 알면 좋았을텐데 싶다. <span style="color:gray">(전기수에 제출한 서류 보며 약간 반성이 되기도 하고...)</span> 
보는 입장에서도 절실한 사람을 더 뽑고싶은게 당연하니까 말이다.</p>
<p>그리고 자신을 치켜세우는 것도 좋지만 그렇다고 남을 깎아내리는 멘트 또한 좋게 보이지 않았다.
팀원들이 아무것도 안해서 총대메고 캐리했다...부족한 팀원들을 가르쳤다... 등 의도는 알겠으나 더 좋은 표현으로 어필하면 좋을 듯 하다.</p>
<blockquote>
<p>물론 팀 활동에서 혼자만 작업하는 것이 힘든 것도 알고, 원치 않게 이끌어야하는 상황에서 성공적으로 마무리하는 것은 엄청난 일인 것도 안다!
하지만 그 부분을 어필하는데 워딩이 뭔가 걸린다...하지만 더 순화할 방법이 생각나지 않는다 하면 굳이 상세하게 명시하지 않아도 된다.
지원서에 장황하게 잘했다고 써도 커밋 부검하면서 실제 기여도 체크하고, 굳이 언급하지 않은 프로젝트라도 찾아내서 지원서는 담백한데 포폴이랑 에티튜드가 굉장하네~ 하며 뜯어보는게 면접관이니까 😭</p>
</blockquote>
<p>이정도만 걸러도 1/3은 걸러지는데, 즉슨 나머지 2/3은 정성껏 쓰고 열심히 준비한 사람들뿐이고 우리도 최선을 다해 선별해야한다는 뜻이다.</p>
<p>이때부터는 <strong>프로젝트 기여도, 협업 습관, 성실성</strong> 등을 중점적으로 평가했다.
README 수정, lint 등으로 커밋하고는 메인 프로젝트로 올려두거나 1일1커밋을 언급한 경우, 커밋 메세지나 브랜치/이슈 관리 등이 제대로 되지 않는 경우 등이 주로 당락을 결정지은 포인트였다.
(의외로 이런 사람이 꽤 많았다)</p>
<p><strong>현업자의 경우</strong>에는 포폴 관리가 제대로 되어있지 않거나, 개발 경력이 있음에도 챌린지 포인트나 성장 요소가 전혀 없어서 학생 지원자와 비교해도 전혀 메리트가 없는 경우 후순위로 밀렸다.
또한 22기의 키워드 중 하나가 <strong><code>열정</code></strong>인 만큼, 책임감을 가지고 끝까지 팀과 함께 서비스를 완성하려는 마음이 있는 사람들에게 좋은 평가를 주었다.</p>
<p align="center" style="color:gray">
 <img style="margin:50px 0 10px 0" src="https://velog.velcdn.com/images/_koiil/post/6314c551-7aa1-45bf-891a-2801885fdbfb/image.png" alt="factorio thumbnail" width=300 />
  </p>


<p>물론 평가자가 나 혼자가 아니고, 다른 운영진들에게는 좋은 점수를 받아서 면접에 온 사람도 있었다.</br>
여러 관점에서 누군가는 정성적으로, 누군가는 정량적으로 평가하여 합불을 결정했기 때문에 나름대로 공정하게 평가되었지 않나 싶다.</p>
<p>**
직접 면접관의 입장이 되는 경험도 굉장히 큰 소득이기는 하지만, 운영진을 하며 가장 좋았던 중 하나가 바로 남녀노소 경력을 불문하고 열심히 살아가는 지원자들을 보며 스스로도 많이 자극받을 수 있었다는 것이다 🔥**</p>
<h3 id="⏰-운영"><code>⏰ 운영</code></h3>
<p>사실 CTO라는 직책때문에 개발 많이 하겠지? 하고 싱글벙글했는데, CTO도 운영진이다.... 운영이 너무 많다. CEO나 COO 에 비해서는 세션기간에는 상대적으로 업무가 적지만, 그래도...많다...ㅜㅜ
(회사 업무와 병행하려면 정말 쉬는날 없이 일해야한다)</p>
<p>후기글이므로 상세하게 어떤 프로세스로 업무가 진행되는 지는 적지 않지만, CTO 의 업무를 대략적으로 리스트업하면 아래와 같다.</p>
<pre><code>- 리쿠르팅, 이수증, 수료증 등 공지 메일 서비스 개편
- 매주 회의 및 세션 준비
- 서버 버그 픽스 및 홈페이지 수정
- 자체 서비스 권한 관리
- 인프라 개선
- 외부 협업사 미팅
- 회원용 클라우드 크레딧 관리, 기술 지원, 출결 관리
- 몹시 중요한 문서화</code></pre><p>활동 기간 전에는 기술적인 업무를 대부분 파악하고 끝내두어야하고, 활동 중에는 다른 운영진들을 도우며 짬짬히 개편 작업 + 후원사 미팅을 진행하게 된다.</p>
<p>특히나 매 기수 운영진을 선출하는 넥스터즈 특성상 히스토리 전달이 정말 중요한데, 무슨 원피스도 아니고 중간에 잃어버린 n기수의 기간이 있어서 직접 서버를 뒤져가며 모든걸 찾아야해서 죽을 맛이었다.. (이 삽질기는 운영 문서로 정리해두었으니 후임들에게 도움이 되었으면 한다🥲)</p>
<p>뭐 결론적으로는 임기동안 하려던 일을 80%는 해치웠고, 말도 많고 탈도 많은 기간이었지만 놀기 좋아하고 그만큼 일도 열심히 하는 횐님덜🔥 덕에 나름 좋은 프로덕트들이 나오며 잘 마무리된 듯 하다. 
남은 태스크는 빠이팅넘치는 23기 운영진들에게 맡기고 노친네는 이제 쉬러 간다...</p>
<hr>
<p>임기동안 옆에서 같이 고생하는 CEO,COO,CDO,CMO 를 보며 전 기수에 내가 즐겼던 동아리는 이런 피땀눈물로 이루어지는구나 싶었다.
텅 빈 대관장소에서 자리를 정리하고 있으면 운영건과 세션을 준비하면서 녹아없어진 주말에 허무하기도 하지만, 그래도 준비한 세션이 잘 마무리되고 이번주도 성공적으로 끝냈다! 라는 생각이 들때면 뿌듯함을 느끼기도 했다.</p>
<p>정말 쉬는 날 없이 고생했지만, 누군가가 동아리 운영진 어떠냐고 묻는다면 한번쯤은 꼭 해보기를 추천하고 싶다. 그리고 운영진이 아니더라도, 동아리는 아주아주 추천한다!</p>
<p>이렇게 다양한 사람들과 만나서 협업하고, 뒷풀이와 여러 행사를 함께하며 친해질 수 있는 기회를 마다할 이유가 없지않나 싶다.
이번 분기는 한번 쉬고, 다음 기수에 다시 놀러가야지🙄</p>
]]></description>
        </item>
    </channel>
</rss>