<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Troubleshooting on Gukin Han</title>
    <link>https://gukin.dev/tags/troubleshooting/</link>
    <description>Recent content in Troubleshooting on Gukin Han</description>
    <generator>Hugo</generator>
    <language>ko-kr</language>
    <lastBuildDate>Sun, 23 Nov 2025 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://gukin.dev/tags/troubleshooting/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Fat 메시지로 인한 Consumer 메모리 누수 케이스</title>
      <link>https://gukin.dev/posts/kafka-fat-message-consumer-memory-leak/</link>
      <pubDate>Sun, 23 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://gukin.dev/posts/kafka-fat-message-consumer-memory-leak/</guid>
      <description>&lt;h2 id=&#34;문제&#34;&gt;문제&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;현상:&lt;/strong&gt; 8개 Fargate Task 중 1개만 메모리 사용률이 지속 상승 (60% → 97%)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;결과:&lt;/strong&gt; 메모리 알람 후 해당 Task 재시작&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;특이점:&lt;/strong&gt; 동일 코드 기반의 다른 Task들은 GC 정상 작동&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;환경:&lt;/strong&gt; Java 21, Spring Boot, Kafka Consumer, MyBatis Batch, AWS Fargate&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;증상-분석&#34;&gt;증상 분석&lt;/h2&gt;
&lt;p&gt;&lt;img alt=&#34;원본 데이터로 부터 재 생성된 그래프&#34; loading=&#34;lazy&#34; src=&#34;https://gukin.dev/posts/kafka-fat-message-consumer-memory-leak/metrics_matplotlib_composite.png&#34;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CloudWatch 메모리 그래프: 특정 Task만 계단식 상승&lt;/li&gt;
&lt;li&gt;덤프 서버: 동일 이벤트 처리에도 톱니형 GC 패턴 (정상)&lt;/li&gt;
&lt;li&gt;GC 자체 문제라기보다는 &lt;strong&gt;객체 참조 유지로 인한 회수 불가&lt;/strong&gt; 상황에 가까움&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;원인-분석&#34;&gt;원인 분석&lt;/h2&gt;
&lt;h3 id=&#34;대량-청크-메시지&#34;&gt;대량 청크 메시지&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;권한 갱신 이벤트를 한 번에 2,500건 단위로 Kafka에 게시&lt;/li&gt;
&lt;li&gt;특정 Task가 해당 청크를 독점 처리하면 순간적으로 수만 개 객체 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;중복-역직렬화&#34;&gt;중복 역직렬화&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Consumer 내부에서 &lt;code&gt;Map → DTO → VO&lt;/code&gt; 변환을 최대 세 번 반복&lt;/li&gt;
&lt;li&gt;일시적 객체 폭증, Eden → Old Gen 승격 가속&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;mybatis-batch-누적&#34;&gt;MyBatis Batch 누적&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ExecutorType.BATCH&lt;/code&gt; 사용 시 &lt;code&gt;flushStatements()&lt;/code&gt; 전까지 파라미터 참조 유지&lt;/li&gt;
&lt;li&gt;GC 입장에서는 여전히 &amp;ldquo;사용 중&amp;rdquo; 객체로 인식&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;컨슈머-병렬성-부족&#34;&gt;컨슈머 병렬성 부족&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;단일 스레드 리스너로 동작&lt;/li&gt;
&lt;li&gt;특정 파티션이 한 Task에 몰리면 부하 편향 및 메모리 사용량 집중&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;gc-관점에서-정리&#34;&gt;GC 관점에서 정리&lt;/h2&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;구분&lt;/th&gt;
          &lt;th&gt;설명&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;GC는 참조가 끊긴 객체만 수거&lt;/td&gt;
          &lt;td&gt;리스트, 세션 등에서 참조 중이면 회수 불가&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;부하가 높으면 safepoint 진입 지연&lt;/td&gt;
          &lt;td&gt;GC 실행 타이밍이 밀리면서 Old Gen 누적&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Old Gen 승격 가속&lt;/td&gt;
          &lt;td&gt;장수 객체가 많아질수록 Old Gen 압박 증가&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;덤프 서버에서 정상인 이유&lt;/td&gt;
          &lt;td&gt;부하가 낮아 GC 개입 여유가 충분&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;단기-개선-해결책&#34;&gt;단기 개선 해결책&lt;/h2&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;조치&lt;/th&gt;
          &lt;th&gt;설명&lt;/th&gt;
          &lt;th&gt;기대 효과&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;DTO 직접 바인딩&lt;/td&gt;
          &lt;td&gt;@KafkaListener에서 Map 대신 DTO로 수신&lt;/td&gt;
          &lt;td&gt;중복 역직렬화 제거&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;청크 분할 처리&lt;/td&gt;
          &lt;td&gt;2,500건 → 500건 단위로 분할 처리&lt;/td&gt;
          &lt;td&gt;동시 생성 객체 수 감소, 생존 시간 단축&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;리스트 참조 해제&lt;/td&gt;
          &lt;td&gt;처리 후 리스트 &lt;code&gt;clear()&lt;/code&gt; 또는 참조 &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;GC 회수 가능 시점 앞당김&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Batch flush 주기 조정&lt;/td&gt;
          &lt;td&gt;200건마다 &lt;code&gt;flushStatements()&lt;/code&gt; 호출&lt;/td&gt;
          &lt;td&gt;MyBatis 내부 파라미터 참조 조기 해제&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;필드 누락 복구&lt;/td&gt;
          &lt;td&gt;누락된 &lt;code&gt;accessType&lt;/code&gt; 필드 복원&lt;/td&gt;
          &lt;td&gt;불필요한 대량 delete 방지&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;장기-개선-해결책&#34;&gt;장기 개선 해결책&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Kafka Consumer 병렬성 향상 (&lt;code&gt;factory.setConcurrency(2)&lt;/code&gt; 등)&lt;/li&gt;
&lt;li&gt;Partition assignment 전략을 &lt;code&gt;cooperative-sticky&lt;/code&gt;로 조정&lt;/li&gt;
&lt;li&gt;DLQ(Dead Letter Topic) 구성으로 재시도 루프 제거&lt;/li&gt;
&lt;li&gt;Micrometer로 배치 크기, 처리 시간, lag 메트릭 수집 및 모니터링&lt;/li&gt;
&lt;/ul&gt;</description>
    </item>
  </channel>
</rss>
