<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>모종닷컴</title>
    <link>https://monny.tistory.com/</link>
    <description>하루는 지나가는 것이 아니라 쌓이는 것이다</description>
    <language>ko</language>
    <pubDate>Sun, 23 Aug 2026 12:55:56 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>모종</managingEditor>
    <image>
      <title>모종닷컴</title>
      <url>https://tistory1.daumcdn.net/tistory/2754989/attach/991f6fb565924e8483f4c95fbf859e5a</url>
      <link>https://monny.tistory.com</link>
    </image>
    <item>
      <title>Docker 이미지 변경 후 TLS Handshake가 깨졌다 (2) &amp;mdash; 정말 TLS_RSA_* 하나가 원인의 전부였을까?</title>
      <link>https://monny.tistory.com/300</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 Docker 이미지를 변경한 뒤 외부 서비스와의 TLS Handshake가 실패했고, JDK 17.0.18부터 java.security의 jdk.tls.disabledAlgorithms에 TLS_RSA_*가 추가된 것이 직접적인 원인이라고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 장애를 빠르게 복구해야 했기 때문에 -Djava.security.properties를 이용해 TLS 정책을 재정의했고, 실제로 문제도 해결되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 장애가 해결된 이후에도 한 가지 의문이 계속 머릿속을 떠나지 않았습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정말 TLS_RSA_* 하나만이 원인이었을까?&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 외부 서비스는 정말 TLS_RSA_*밖에 사용할 수 없는 서버였을까?&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;첫 번째 단서: Cipher Suite 재확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 해당 서버가 실제로 어떤 Cipher Suite를 지원하는지 다시 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sslscan으로 TLSv1.2에서 지원하는 Cipher를 확인해 보니 1편과 같은 결과를 다시 볼 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Preferred TLSv1.2  AES128-GCM-SHA256
Accepted  AES256-GCM-SHA384
Accepted  AES128-SHA
Accepted  AES256-SHA
Accepted  DHE-RSA-AES128-GCM-SHA256
Accepted  DHE-RSA-AES256-GCM-SHA384
Accepted  DHE-RSA-AES128-SHA256
Accepted  DHE-RSA-AES256-SHA256
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 살펴봤던 disabledAlgorithms는 Forward Secrecy가 없는 정적 RSA Key Exchange&lt;b&gt;(TLS_RSA_*)&lt;/b&gt;만 비활성화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 서버는 TLS_DHE_RSA_* 계열도 지원하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;굳이 TLS_RSA_*를 사용하지 않아도 TLS Handshake가 가능해야 하는 것 아닌가?&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 새로운 의문이 생겼습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;JSSE로 직접 연결해 보자&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가설을 검증하기 위해 Spring Boot와 Reactor Netty를 모두 제거하고 순수 JSSE만 사용하여 TLS 연결을 시도했습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;HANDSHAKE SUCCESS
cipher=TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
protocol=TLSv1.2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 17.0.19에서도 TLS_DHE_RSA_*는 아무 문제 없이 사용할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과를 보는 순간 이런 생각이 들었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JVM 보안 정책을 우회할 필요가 없었던 것 아닐까?&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Forward Secrecy를 지원하는 TLS_DHE_RSA_*만 사용하면 disabledAlgorithms를 건드리지 않아도 충분할 것처럼 보였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;WebClient 설정하면서 Cipher Suite가 대체되었을까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 서비스와의 통신을 위해 서버는 webClient를 사용하고 있었습니다. 따라서 다음으로 의심이 가는 부분은 WebClient 설정에서 Cipher Suite가 대체가 된 것이 아닐까 싶었습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783936116664&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun customClientHttpConnector(): ClientHttpConnector {
    val sslContext =
        SslContextBuilder.forClient()
            .protocols(&quot;TLSv1.2&quot;)
            ...
            .build()
    val httpClient = HttpClient.create().secure { it.sslContext(sslContext) }
    return ReactorClientHttpConnector(httpClient)
}

...

private val webClient =
    webClientBuilder
        .baseUrl(&quot;xxx.xxx.xxx&quot;)
        .clientConnector(customClientHttpConnector)
        .exchangeStrategies(
            ExchangeStrategies.builder()
                .codecs { it.defaultCodecs().maxInMemorySize(MAX_BUFFER_BYTES) }
                .build(),
        )
        .build()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드 Netty의 SslContextBuilder로 이 상태의 cipher 후보를 직접 찍어봤습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1783936253268&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;===== Netty engine enabled cipher suites (no explicit .ciphers()) =====
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
Exception in thread &quot;main&quot; javax.net.ssl.SSLHandshakeException: (handshake_failure) Received fatal alert: handshake_failure&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECDHE 계열 6종뿐이었습니다. 이전에 sslscan 목록에 ECDHE가 단 하나도 없었습니다 &amp;mdash; 전부 RSA 정적 키교환 또는 DHE_RSA 뿐이었습니다. 그렇다면 JDK TLS 정책 문제와 상관없이 SslContextBuilder의 Cipher Suite 목록에서 이미 협의할 Cipher가 없기에 TLS 통신이 안된 거라고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 의문이 생겼습니다. &lt;b&gt;이전 코드에서도 WebClient를 사용해서 통신했었는데 이전에는 왜 실패하지 않았을까?&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;이전 코드에서는 왜 정상 동작했을까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 저의 큰 착각이 드러났습니다. 원래 운영 코드에는 TLS 설정이 전혀 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈를 해결하는 과정에서 자연스럽게 SslContextBuilder를 사용하게 되었는데, 이전에는 SslContextBuilder 기반의 ClientHttpConnector를 사용하지 않고 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Bean
fun customWebClient(): WebClient =
    WebClient.builder()
        .baseUrl(&quot;xxx.xxx.xxx&quot;)
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .build()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이전에는 기본 설정(Spring Boot가 만드는 순정 WebClient)으로 정상 동작했다는 뜻입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;기본 설정 Cipher를 확인해 보자&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reactor-netty의 HttpClient.create()가 .secure()를 아예 호출하지 않을 때 실제로 쓰는 SslProvider.defaultClientProvider()를 직접 뽑아봤습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;===== reactor-netty SslProvider.defaultClientProvider() enabled cipher suites =====
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_GCM_SHA256      &amp;larr; Netty raw 기본값엔 없던 항목
TLS_RSA_WITH_AES_128_CBC_SHA        &amp;larr; 
TLS_RSA_WITH_AES_256_CBC_SHA        &amp;larr;
TLS_AES_128_GCM_SHA256   (TLS 1.3)
TLS_AES_256_GCM_SHA384   (TLS 1.3)
[real socket] handshake SUCCESS, cipher=TLS_RSA_WITH_AES_128_GCM_SHA256, protocol=TLSv1.2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Netty를 직접 호출했을 때의 기본 cipher 목록과, reactor-netty(Spring WebClient가 실제로 쓰는 경로)의 기본 cipher 목록은 서로 달랐습니다.&lt;/b&gt; 후자는 RSA-static 계열까지 포함하고 있고, 그래서 외부 서비스가 가장 선호하는 cipher(AES128-GCM-SHA256)로 정상 협상이 됐습니다. &quot;cipher를 아무것도 안 건드린&quot; 원래 코드가 실제로 작동했던 이유가 바로 이거였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마지막 확인: JDK 패치가 정말 이 목록을 바꾸는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 테스트 코드를 로컬 JDK(17.0.16, 지난 글에서 비교했던 그 버전)와, 실제 운영과 동일한 eclipse-temurin:17-jre-jammy(JDK 17.0.19+10) 컨테이너에서 각각 테스트해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로컬 (JDK 17.0.16)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;TLS_RSA_WITH_AES_128_GCM_SHA256   ✅ 있음
TLS_RSA_WITH_AES_128_CBC_SHA      ✅ 있음
TLS_RSA_WITH_AES_256_CBC_SHA      ✅ 있음
&amp;rarr; handshake SUCCESS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;jammy (JDK 17.0.19+10, 운영과 동일)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
&amp;rarr; SSLHandshakeException: (handshake_failure) Received fatal alert: handshake_failure&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑같은 코드인데, JDK 패치 버전만 다르게 돌렸더니 enabledCipherSuites 목록에서 RSA-static 3종이 통째로 사라졌습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론: 인과관계 전체 그림&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;JDK 17.0.19 이전 (구 alpine)
  reactor-netty 기본 cipher 목록 = ECDHE 6종 + RSA-static 3종 + TLS1.3 2종
  &amp;rarr; 외부 서비스가 선호하는 TLS_RSA_WITH_AES_128_GCM_SHA256로 정상 협상
JDK 17.0.19+10 (jammy)
  disabledAlgorithms가 RSA-static 3종을 enabledCipherSuites에서 제거
  &amp;rarr; 남은 건 ECDHE + TLS1.3뿐인데 외부 서비스는 ECDHE, TLS1.3 를 전혀 지원하지 않음
  &amp;rarr; handshake_failure (실제 장애)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 던졌던 &quot;&lt;b&gt;정말 TLS_RSA_* 하나가 원인의 전부였을까&lt;/b&gt;&quot;라는 의문에 대한 답은 결국 맞았습니다, 그게 유일한 원인이었습니다. 다만 그 과정에서 아무도 몰랐던 사실 두 가지를 덤으로 얻을 수 있었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SslContextBuilder를 직접 호출한 기본값과 HttpClient.create()가 알아서 쓰는 기본값은 서로 다른 cipher 목록을 갖는다 &amp;mdash; 뭔가를 &quot;명시적으로 설정&quot;하는 순간, 의도치 않게 더 안전한(넓은) 기본값에서 이탈할 수 있다.&lt;/li&gt;
&lt;li&gt;JDK tls 정책을 우회하지 않고도 가능한 Cipher Suite가 존재했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후속 조치: JVM 플래그를 걷어내다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS_DHE_RSA_* 계열은 forward secrecy가 있어서 애초에 disabledAlgorithms 제약에 걸리지 않습니다. 따라서 cipher를 이걸로 명시하면 JVM 보안 정책을 건드릴 필요가 없습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// Before
.ciphers(listOf(&quot;TLS_RSA_WITH_AES_128_GCM_SHA256&quot;), SupportedCipherSuiteFilter.INSTANCE)
// After
.ciphers(
    listOf(
        &quot;TLS_DHE_RSA_WITH_AES_128_GCM_SHA256&quot;,
        &quot;TLS_DHE_RSA_WITH_AES_256_GCM_SHA384&quot;,
    ),
    SupportedCipherSuiteFilter.INSTANCE,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글 마지막에 &quot;이게 정말 원인의 전부인지는 좀 더 파봐야 할 것 같다&quot;라고 적었던 게, 결과적으로 원래 진단이 맞았다는 걸 재확인하는 여정이 됐습니다. 다만 그 여정에서 실제 재현 테스트 코드와 프로덕션 코드가 서로 다른 경로를 타고 있었다는 걸 늦게 알아챈 게 가장 뼈아픈 삽질이었습니다 &amp;mdash; 가설을 검증할 땐 반드시 실제 코드 경로를 그대로 태워봐야 한다는 걸 다시 한번 배울 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편으로는 그렇다면 JDK 17.0.18부터 왜 TLS_RSA를 비활성화했을까? 에 대한 글을 이어서 써보도록 하겠습니다.&lt;/p&gt;</description>
      <author>모종</author>
      <guid isPermaLink="true">https://monny.tistory.com/300</guid>
      <comments>https://monny.tistory.com/300#entry300comment</comments>
      <pubDate>Mon, 13 Jul 2026 20:12:55 +0900</pubDate>
    </item>
    <item>
      <title>Docker 이미지 변경 후 TLS Handshake가 깨졌다</title>
      <link>https://monny.tistory.com/299</link>
      <description>&lt;h1&gt;Docker 이미지를 변경했더니 TLS Handshake가 실패했다&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서 Docker Base Image를 변경한 뒤 예상하지 못한 장애를 경험했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 코드도 변경하지 않았고, 외부 서비스도 변경되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 Docker 이미지만 변경했을 뿐인데 특정 외부 서비스와의 HTTPS 통신이 모두 실패하기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 JDK 업데이트로 인해 TLS 정책이 변경된 것이라고 생각했고 가볍게 대처하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제를 추적하는 과정을 통해 정확한 원인을 짚고, 올바른 대처를 할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;장애를 어떻게 분석했고 어떤 가설을 세우며 원인을 좁혀갔는지&lt;/b&gt;를 정리해보려고 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;장애 발생&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 Spring Boot의 WebClient를 사용하여 외부 기관과 HTTPS 통신을 수행하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Base Image를 변경한 후 기존과 동일한 애플리케이션을 배포했는데, 특정 기관과의 통신에서 아래와 같은 예외가 발생하기 시작했습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;javax.net.ssl.SSLHandshakeException

Received fatal alert: handshake_failure
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흥미로운 점은 다음과 같았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 코드는 변경되지 않았다.&lt;/li&gt;
&lt;li&gt;요청 데이터도 동일했다.&lt;/li&gt;
&lt;li&gt;다른 외부 기관과는 정상적으로 통신된다.&lt;/li&gt;
&lt;li&gt;특정 기관과만 실패한다.&lt;/li&gt;
&lt;li&gt;로컬에서는 정상적으로 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서만 특정 외부 서비스와 TLS Handshake가 실패하는 상황이었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;TLS Handshake가 왜 실패하는 걸까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSLHandshakeException이라는 예외만으로는 원인을 알 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS Handshake 실패는 다양한 이유로 발생할 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증서 문제&lt;/li&gt;
&lt;li&gt;TLS Version 불일치&lt;/li&gt;
&lt;li&gt;Cipher Suite 협상 실패&lt;/li&gt;
&lt;li&gt;Trust Store 문제&lt;/li&gt;
&lt;li&gt;SNI 문제&lt;/li&gt;
&lt;li&gt;서버 정책 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등 여러 가능성이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 TLS Debug 로그를 켜는 것보다 먼저 &lt;b&gt;외부 서버가 어떤 TLS 환경을 제공하는지 확인&lt;/b&gt;하기로 했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;OpenSSL로 TLS 연결 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 OpenSSL을 이용하여 외부 서버와 직접 TLS 연결을 시도했습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;openssl s_client -connect xxx.xxx.xxx:443&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 정상적으로 연결되었습니다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;Verification: OK

Protocol : TLSv1.2
Cipher   : AES128-GCM-SHA256
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증서는 정상&lt;/li&gt;
&lt;li&gt;TLS 연결도 정상&lt;/li&gt;
&lt;li&gt;TLS 1.2로 통신 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 사실을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적어도 서버 자체가 죽어있거나 인증서가 만료된 것은 아니었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;서버는 어떤 Cipher Suite를 지원할까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 확인한 것은 서버가 지원하는 Cipher Suite였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 sslscan을 사용했습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sslscan xxx.xxx.xxx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 보면 TLS 1.2에서 다음과 같은 Cipher Suite를 지원하고 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Preferred TLSv1.2

AES128-GCM-SHA256

Accepted
AES256-GCM-SHA384
AES128-SHA
AES256-SHA
DHE-RSA-AES128-GCM-SHA256
DHE-RSA-AES256-GCM-SHA384
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 사실은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 TLS 1.2를 지원하며, 여러 종류의 Cipher Suite를 지원한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서는 아직 어느 Cipher가 실제로 선택되는지, 어떤 Cipher 때문에 Handshake가 실패하는지는 알 수 없었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Docker 이미지가 바뀌면서 무엇이 달라졌을까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 코드는 변경되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot도 동일했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor Netty 버전도 동일했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 가장 큰 차이는 Docker Base Image에 포함된 &lt;b&gt;JDK 버전&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 이미지는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;JDK 17.0.16
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 이미지는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;JDK 17.0.19
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 자연스럽게&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK의 TLS 정책이 변경된 것은 아닐까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 가설을 세우게 되었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;TLS Debug 로그 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS Debug 로그를 활성화했습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;-Djavax.net.debug=ssl,handshake
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 로그를 출력해보면 수천 줄의 로그가 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 로그 초반부터 반복해서 나타나는 내용이 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;Ignore disabled cipher suite:

TLS_RSA_WITH_AES_128_GCM_SHA256

Ignore disabled cipher suite:

TLS_RSA_WITH_AES_256_GCM_SHA384

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK가 일부 Cipher Suite를 협상 후보에서 제외하고 있다는 의미였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;JDK 정책 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 JDK의 java.security 파일을 확인해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 17.0.19에는 다음과 같은 설정이 존재했습니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;jdk.tls.disabledAlgorithms=
...
TLS_RSA_*
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS_RSA_* Cipher Suite가 기본적으로 비활성화되도록 변경된 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 결과를 종합하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenSSL에서는 정상 연결된다.&lt;/li&gt;
&lt;li&gt;JDK는 TLS_RSA_*를 비활성화한다.&lt;/li&gt;
&lt;li&gt;TLS Handshake는 실패한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지 사실을 확인할 수 있었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;당시 내렸던 결론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서 저는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JDK가 TLS_RSA를 기본적으로 비활성화하면서 TLS Handshake가 실패한 것&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 운영 환경에서는 java.security 정책을 우회하여 TLS_RSA_*를 다시 사용할 수 있도록 설정했고, 실제로 장애도 해결되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 장애를 빠르게 복구해야 하는 상황에서는 충분히 합리적인 판단이었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;그런데 정말 원인은 TLS_RSA였을까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애는 해결되었지만 한 가지 의문이 계속 남아있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대 서버는 여러 Cipher Suite를 지원하고 있는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 TLS_RSA_* 하나만 사용할 수 있었던 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 JDK의 보안 정책이 아니라 다른 원인이 있었던 것은 아닐까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 운영 장애가 해결된 이후, 문제를 처음부터 다시 재현하며 하나씩 검증해보기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 순수 JSSE와 Netty를 각각 이용하여 동일한 환경에서 TLS Handshake를 재현해보면서, &lt;b&gt;왜 JSSE는 성공했는데 Netty(WebClient)는 실패했는지&lt;/b&gt;, 그리고 실제 원인이 무엇이었는지를 자세히 살펴보겠습니다.&lt;/p&gt;</description>
      <category>Programming/JAVA</category>
      <author>모종</author>
      <guid isPermaLink="true">https://monny.tistory.com/299</guid>
      <comments>https://monny.tistory.com/299#entry299comment</comments>
      <pubDate>Sun, 12 Jul 2026 20:40:20 +0900</pubDate>
    </item>
    <item>
      <title>MacOS F1~F12 기본 기능키로 바꾸기</title>
      <link>https://monny.tistory.com/297</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;macOS에서는 기본적으로 F1~F12 키가 &amp;lsquo;특수 기능 키(밝기, 소리, 재생 등)&amp;rsquo;로 동작하고, 실제 F1~F12 기능을 쓰려면 Fn 키(또는 Globe 키=지구모양 키)를 함께 눌러야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MacOS를 사용하다보면 &quot;특수 기능&quot;보다는 &quot;기본 기능&quot;을 많이 사용하므로, 저의 경우 F?? 키를 눌렀을 때 기본 기능키로 바꾸는게 적합한 것 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 키보드 단축키 설정 들어가기&lt;br /&gt;&quot;시스템 설정&quot; -&amp;gt; 좌측 &quot;키보드&quot; 메뉴 -&amp;gt; &quot;키보드 단축키&quot;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;1254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ozrVw/dJMcaeFZaIV/ZSXwSNvrv6GFl41QKStHzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ozrVw/dJMcaeFZaIV/ZSXwSNvrv6GFl41QKStHzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ozrVw/dJMcaeFZaIV/ZSXwSNvrv6GFl41QKStHzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FozrVw%2FdJMcaeFZaIV%2FZSXwSNvrv6GFl41QKStHzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;536&quot; height=&quot;462&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;1254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. F? 키를 표준 기능키로 사용 활성화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌측 &quot;기능 키&quot; -&amp;gt; 기능 활성화&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqfLPm/dJMcaiax1Hs/6iZENUKtkxF9O8x5E6Cw30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqfLPm/dJMcaiax1Hs/6iZENUKtkxF9O8x5E6Cw30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqfLPm/dJMcaiax1Hs/6iZENUKtkxF9O8x5E6Cw30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqfLPm%2FdJMcaiax1Hs%2F6iZENUKtkxF9O8x5E6Cw30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;543&quot; height=&quot;375&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능을 활성화 한 이후에 특수 기능(예: F11(소리 줄이기))을 사용하려면 설명에 나와있듯이 &quot;Fn&quot; 키를 누르면 됩니다.&lt;/p&gt;</description>
      <category>MacOS</category>
      <author>모종</author>
      <guid isPermaLink="true">https://monny.tistory.com/297</guid>
      <comments>https://monny.tistory.com/297#entry297comment</comments>
      <pubDate>Sun, 2 Nov 2025 21:15:16 +0900</pubDate>
    </item>
    <item>
      <title>컨테이너 환경에 따른 GC 전략</title>
      <link>https://monny.tistory.com/296</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;도커 컨테이너로 자바 애플리케이션을 실행한 후 모니터링을 하다보니 제가 알고 있던 가비지 컬렉션 동작과는 다르다는 것을 발견했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 Oracle Linux Server 리눅스 서버와 openjdk 21를 사용했습니다. 추가로 컨테이너 메모리를 1GB로 제한하였습니다. 이렇게 구성하였을 때 아래와 같이 GC 로그와 자바 버전을 출력해보았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1760879882948&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bash-4.4# java -Xlog:gc --version
[0.002s][info][gc] Using Serial
openjdk 21 2023-09-19
OpenJDK Runtime Environment (build 21+35-2513)
OpenJDK 64-Bit Server VM (build 21+35-2513, mixed mode, sharing)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 첫 번째 줄을 보니 GC 전략이 Serial로 되있다는 것을 발견하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java Ergonomics&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/ergonomics.html#GUID-DA88B6A6-AF89-4423-95A6-BBCBD9FAE781&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HotSpot Virtual Machine Garbage Collection Tunning Guide&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760880079553&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;HotSpot Virtual Machine Garbage Collection Tuning Guide&quot; data-og-description=&quot;Ergonomics is the process by which the Java Virtual Machine (JVM) and garbage collection heuristics, such as behavior-based heuristics, improve application performance.&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/ergonomics.html#GUID-DA88B6A6-AF89-4423-95A6-BBCBD9FAE781&quot; data-og-url=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/ergonomics.html#GUID-DA88B6A6-AF89-4423-95A6-BBCBD9FAE781&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/ergonomics.html#GUID-DA88B6A6-AF89-4423-95A6-BBCBD9FAE781&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/ergonomics.html#GUID-DA88B6A6-AF89-4423-95A6-BBCBD9FAE781&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;HotSpot Virtual Machine Garbage Collection Tuning Guide&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Ergonomics is the process by which the Java Virtual Machine (JVM) and garbage collection heuristics, such as behavior-based heuristics, improve application performance.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉터, 힙 크기 및 런타임 컴파일러 기본 선택 사항에 대한 내용을 담고 있는 내용입니다. 아래와 같이 정리할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버급 시스템에서는 G1 컬렉터, 그렇지 않으면 Serial 컬렉터.&lt;/li&gt;
&lt;li&gt;최대 GC 스레드 수는 힙 크기와 사용 가능한 CPU 리소스에 의해 제한&lt;/li&gt;
&lt;li&gt;초기 힙 크기 : 물리적 메모리의 1/64&lt;/li&gt;
&lt;li&gt;최대 힙 크기는 실제 메모리의 1/4&lt;/li&gt;
&lt;li&gt;계층형 컴파일어, C1과 C2 모두 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목했던 건 당연히 첫 번째 항목입니다. 여기서 말하는 서버급 시스템의 기준은 무엇일까요? 이에 대한 내용도 언급되어 있는데 VM이 두 개 이상의 프로세서와 1792MB 이상의 힙 크기를 감지하면 VM은 머신을 서버급으로 간주한다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 위에서 제가 경험했던 문제는 컨테이너에 1GB 메모리 제한을 걸게됨으로서 위에서 얘기하는 서버급 시스템이 아니게 되어 Serial 컬렉터가 사용됬다라는 것을 알 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 도커 컨테이너 메모리 제한을 2GB로 설정한 후 다시 GC 로그를 출력해보면 아래와 같이 G1 컬렉터가 사용되는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1760880357849&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bash-4.4# java -Xlog:gc --version
[0.002s][info][gc] Using G1
openjdk 21 2023-09-19
OpenJDK Runtime Environment (build 21+35-2513)
OpenJDK 64-Bit Server VM (build 21+35-2513, mixed mode, sharing)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Programming/JAVA</category>
      <author>모종</author>
      <guid isPermaLink="true">https://monny.tistory.com/296</guid>
      <comments>https://monny.tistory.com/296#entry296comment</comments>
      <pubDate>Thu, 30 Oct 2025 18:03:43 +0900</pubDate>
    </item>
    <item>
      <title>Compressed Class 영역</title>
      <link>https://monny.tistory.com/295</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Container에 리소스 제한 설정을 CPU(1.0), Memory(1GB)로 설정한 후 JVM 메트릭을 확인하는데 Non-heap 영역이 1GB가 넘게 할당되있는 것을 봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m759e/btsQ52feUFs/uDTDahWKXRVRtI8Lu5uwsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m759e/btsQ52feUFs/uDTDahWKXRVRtI8Lu5uwsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m759e/btsQ52feUFs/uDTDahWKXRVRtI8Lu5uwsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm759e%2FbtsQ52feUFs%2FuDTDahWKXRVRtI8Lu5uwsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;474&quot; height=&quot;309&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리는 일단 아래와 같이 할당되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JVM Heap : 248MiB&lt;/li&gt;
&lt;li&gt;JVM Non-Heap : 1.23GiB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 컨테이너 메모리 제한을 잘못설정한건지 리눅스 cgroup을 봤지만 리소스 제한은 잘 된것으로 보였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brtFkY/btsQ5bJ9rE7/1gm8n2yxvBhMUaAvahuP9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brtFkY/btsQ5bJ9rE7/1gm8n2yxvBhMUaAvahuP9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brtFkY/btsQ5bJ9rE7/1gm8n2yxvBhMUaAvahuP9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrtFkY%2FbtsQ5bJ9rE7%2F1gm8n2yxvBhMUaAvahuP9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;470&quot; height=&quot;99&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 메모리는 1GB로 설정했는데 어떻게 된 일 일까요??&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Compressed Class 영역&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM Non-Heap을 들여다 보니 metaspace가 -1B, Compressed Class: 1GiB가 할당되었고, 나머지 240MiB는 기타 다른 영역에 할당됬습니다.&lt;br /&gt;결국 이 Compressed Class라는 영역이 엄청크게 할당되있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compressed Class 영역에 대하여 알아봐야겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UseCompressedOops + UseCompressedClassPointers 활성화시 클래스 메타데이터를 위해 네이티브 메모리 상에서 논리적으로 두 개의 다른 영역이 사용됩니다.&lt;/li&gt;
&lt;li&gt;일반적인 메타데이터는 Metaspace 영역, 클래스 참조용 포인터는 Compressed Class 영역에 저장되게 됩니다.&lt;/li&gt;
&lt;li&gt;CompressedClassPointer는 64비트 프로세스 환경에서 클래스 포인터를 표현할 때 32비트 오프셋을 사용하여 표현됩니다.&lt;/li&gt;
&lt;li&gt;CompressedClass 영역의 사이즈 디폴트값은 1GB (-XX:CompressedClassSpaceSize)로 설정되어 있습니다.&lt;/li&gt;
&lt;li&gt;클래스 포인터를 위한 공간은 최초 예약 공간이며 필요에 따라 커밋됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxF2Mk/btsQ6ONYQQk/R6ZBdSNcMFYFbxUk5JvdoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxF2Mk/btsQ6ONYQQk/R6ZBdSNcMFYFbxUk5JvdoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxF2Mk/btsQ6ONYQQk/R6ZBdSNcMFYFbxUk5JvdoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxF2Mk%2FbtsQ6ONYQQk%2FR6ZBdSNcMFYFbxUk5JvdoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;293&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면 Compressed Class 영역은 클래스 포인터가 저장되는 영역이고, 디폴트가 1GB로 설정되있다는 것을 알 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가 의문: Compressed Class Pointer 방식이 왜 필요한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;64비트 컴퓨터의 주소 참조를 위해 64비트 포인터를 사용할 수 있습니다. 하지만 대부분 애플리케이션은 64비트 주소 공간 전체를 다 쓰는 경우는 없기 때문에 64비트를 낭비하지 않고, 더 작은 32비트 포인터로 대체하기 위함인것으로 보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가 의문: OS는 왜 1GB 넘는 공간을 요구하는 애플리케이션을 실행시켜준걸까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compressed Class 영역은 해당 공간을 예약만 하는 가상 주소이지 실제 물리 메모리를 즉시 할당(commit)하지 않기 때문에 실행시켜준게 아닐까 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 JVM 메트릭에서 1.23 GiB가 할당된 상태로 애플리케이션이 올라간것처럼 보이지만 실제 메모리에 할당된 상태는 아니라고 볼 수 있을 것 같습니다.&lt;/p&gt;</description>
      <category>Programming/JAVA</category>
      <author>모종</author>
      <guid isPermaLink="true">https://monny.tistory.com/295</guid>
      <comments>https://monny.tistory.com/295#entry295comment</comments>
      <pubDate>Sun, 19 Oct 2025 22:10:29 +0900</pubDate>
    </item>
    <item>
      <title>10,000 foot view</title>
      <link>https://monny.tistory.com/294</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dprsek/btsQ7AhwjFr/askLT6k7akpx2qCKVtVXlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dprsek/btsQ7AhwjFr/askLT6k7akpx2qCKVtVXlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dprsek/btsQ7AhwjFr/askLT6k7akpx2qCKVtVXlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdprsek%2FbtsQ7AhwjFr%2FaskLT6k7akpx2qCKVtVXlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;486&quot; height=&quot;102&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NATS 보다가 &quot;10,000 foot view&quot;가 나와서 뭔가 싶었는데&amp;nbsp;비행기가 10,000피트 상공에서 아래를 내려다보는 것처럼, 아주 높은 관점에서 전체적인 그림만 보는 개요(overview)를 뜻한다고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세부적인 기술적 내용이나 구현 방법이 아닌, 어떤 문제를 해결하려고 하는지 혹은 큰 구조가 어떻게 되는지 등 &quot;큰 그림&quot;을 설명할 때 쓰는 말입니다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 비슷한 표현들이 있나 좀 더 찾아봤습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;직역&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;30,000 foot view / 50,000 foot view&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;3만/5만 피트 상공에서 본 관점&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;10,000피트보다 더 높은 곳에서 바라본다는 뜻으로, 매우 거시적인 시각으로 전체를 바라본다는 뜻입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Big picture&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;큰 그림&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;가장 흔한 표현으로 세부보단 전체 흐름을 보자는 뜻입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;High-level overview&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;고수준 개요&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;기술 문서나 회의에서 요약 설명을 할 때 자주 사용되는 표현입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Bird's eye view&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;새의 눈에서 본 관점&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;10,000 foot view와 비슷한 맥락입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>기술 용어</category>
      <author>모종</author>
      <guid isPermaLink="true">https://monny.tistory.com/294</guid>
      <comments>https://monny.tistory.com/294#entry294comment</comments>
      <pubDate>Sun, 12 Oct 2025 01:55:50 +0900</pubDate>
    </item>
    <item>
      <title>Spring EventListener do not working</title>
      <link>https://monny.tistory.com/293</link>
      <description>&lt;pre&gt;&lt;code&gt;@Component
class Test(...) {
    init {
        eventPublihser.publishEvent(TestEvent())
    }
}


@Service
class TestService {
  @EventListener(TestEvent::class)
  fun handleEvent(event: TestEvent) {
      log.info { &amp;quot;hello&amp;quot; }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;이슈&lt;/h2&gt;
&lt;p&gt;위와 같은 코드를 작성했을 때 애플리케이션이 켜지면 “hello”가 출력되기를 기대했으나 출력이 안됨.  &lt;/p&gt;
&lt;p&gt;반면 API를 하나 만들어서 동일한 코드를 실행하면 Listener에서 감지가 된다.  &lt;/p&gt;
&lt;h2&gt;원인&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/** Class SimpleApplicationEventMulticaster **/

    @Override
    public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : ResolvableType.forInstance(event));
        Executor executor = getTaskExecutor();
        for (ApplicationListener&amp;lt;?&amp;gt; listener : getApplicationListeners(event, type)) {
            if (executor != null &amp;amp;&amp;amp; listener.supportsAsyncExecution()) {
                try {
                    executor.execute(() -&amp;gt; invokeListener(listener, event));
                }
                catch (RejectedExecutionException ex) {
                    // Probably on shutdown -&amp;gt; invoke listener locally instead
                    invokeListener(listener, event);
                }
            }
            else {
                invokeListener(listener, event);
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;애플리케이션 init 에서 publishEvent와 api로 publishEvent 한 것이 어떤 차이가 나는지를 확인해보기 위해 디버깅을 해보면 위 동작이 달라진다.  &lt;/li&gt;
&lt;li&gt;애플리케이션 시작시 publishEvent 호출은 for문의 &lt;code&gt;getApplicationListeners(event, type)&lt;/code&gt; 결과는 0이고 API로 호출했을 때는 1이 나오면서 &lt;code&gt;invoke(listener, event)&lt;/code&gt;가 실행된다.  &lt;/li&gt;
&lt;li&gt;위 코드는 특별한 로직이 아니다. 이벤트를 처리할 수 있는 리스너를 리턴하면 해당 리스너들에게 이벤트를 전달하는 로직이다.  &lt;/li&gt;
&lt;li&gt;init {} 코드가 실행되는 시점에는 리스너가 없었다가 api 호출 시점에는 리스너가 생겼다??  &lt;/li&gt;
&lt;li&gt;여기서 해답을 찾았는데 init이 실행된 타이밍에 TestService의 listener 등록이 늦게 된 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/** Class AbstractApplicationEventMulticaster **/
    @Override
    public void addApplicationListener(ApplicationListener&amp;lt;?&amp;gt; listener) {
        synchronized (this.defaultRetriever) {
            // Explicitly remove target for a proxy, if registered already,
            // in order to avoid double invocations of the same listener.
            Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
            if (singletonTarget instanceof ApplicationListener) {
                this.defaultRetriever.applicationListeners.remove(singletonTarget);
            }
            this.defaultRetriever.applicationListeners.add(listener);
            this.retrieverCache.clear();
        }
    }&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;리스너를 조회하는 코드를 타면 결국 this.defaultRetriever.applicationListeners 로부터 가져오는데 코드 마지막쯤 add코드가 보인다. 이 라인에 브레이크포인트를 걸고 condition을 입력한다.  &lt;ul&gt;
&lt;li&gt;listener.getClass() == ApplicationListenerMethodAdapter.class &amp;amp;&amp;amp; ((ApplicationListenerMethodAdapter) listener).beanName.equals(&amp;quot;&amp;quot;)  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;애플리케이션을 실행해보면 init {..} 코드가 먼저 실행되고 이후에 listeners.add 브레이크 포인트가 동작한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Programming/Spring</category>
      <author>모종</author>
      <guid isPermaLink="true">https://monny.tistory.com/293</guid>
      <comments>https://monny.tistory.com/293#entry293comment</comments>
      <pubDate>Sat, 11 Oct 2025 19:48:00 +0900</pubDate>
    </item>
    <item>
      <title>Quorum based Controller</title>
      <link>https://monny.tistory.com/292</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;브로커와 컨트롤러&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 버전 2.8 이후부터 메타데이터 관리를 주키퍼를 사용하지 않고 자체 관리하게 되었는데 이로 인해&lt;u&gt; 카프카 애플리케이션의 역할은 크게 두 가지(브로커, 컨트롤러)로 구분&lt;/u&gt;할 수 있다. '컨트롤러'는 클러스터 수준의 메타데이터 관리 및, 리더 선출, 브로커 추가 제거 같은 클러스터 관리 작업을 하는 역할을 하며, '브로커'는 메시지의 저장 및 전송과 관련된 작업을 담당한다. 애플리케이션을 역할을 지정할 수 있는데, 브로커나 컨트롤러 중 하나의 역할만 지정할수도 있고, 브로커 겸 컨트롤러 역할도 지정할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Quorum based Controller&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;KRaft 합의 알고리즘을 통해 &lt;u&gt;리더로 선출된 컨트롤러는 Active Controller&lt;/u&gt;라고 불리며, &lt;u&gt;그 외에 컨트롤러는 Follower Controller&lt;/u&gt;라고 불린다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기존 카프카의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;__consumer_offsets 토픽이나&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;트랜잭션 그룹 로그(__transaction_state)같이 &lt;u&gt;메타데이터도 하나의 토픽(&lt;/u&gt;&lt;/span&gt;&lt;u&gt;__cluster_metadata)을 가지게 되며, 이를 통해 메타데이터를 관리&lt;/u&gt;하게 된다. 기존에 주키퍼에 저장되던 모든 메타데이터들이 이 토픽에 저장되게 된다. &lt;u&gt;메타데이터 토픽의 리더는 당연히 Active Controller&lt;/u&gt;가 된다.&lt;/li&gt;
&lt;li&gt;&lt;u&gt;쿼럼에서는 주키퍼와 마찬가지로 실행이 유지되기 위해서는 노드의 과반수 이상이 실행되어있어야 한다.&lt;/u&gt; 이를 다시 말하자면 3개의 쿼럼 구성에서는 한 번의 장애를 , 5개의 노드 구성에서는 2번의 장애를 극복할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메타데이터 복제 프로세스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KRaft 알고리즘을 통해 리더를 선출하고 리더 컨트롤러가 메타데이터 로그의 리더로 메타데이터를 관리한다는 것을 알았다면 실제 메타데이터가 어떤 과정을 통해 팔로워에게 복제되는지를 알아보면 좋을 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메타데이터 저장&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 메타데이터에 변경 사항이 생긴다면 Active Controller는 관련 변경 내용을 메타데이터 토픽에 저장한다. 좀 더 정확히는 메타데이터 로그에 추가하기 이전에 내구성 확보를 위해 로컬 디스크(Write-ahead Log, WAL)에 먼저 기록하고 fsync를 한 후 메타데이터 토픽에 저장한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메타데이터 패치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Follower Controller는 주기적으로 이 메타데이터 로그에 추가된 사항이 없는지 Active Controller에게 Fetch 요청을 보내게 되는데, Active Controller는 이 응답으로 추가된 사항들을 넘겨준다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Follower Controller는 응답받은 데이터를 로컬에 적용한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이벤트 /&amp;nbsp; 델타 모델&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;쿼럼에서는 메타데이터 복제에 이벤트/델타 모델 방식을 사용&lt;/u&gt;한다. 특정 메타데이터가 A-&amp;gt;B로 변경되었다는 데이터를 넘겨주는 것이 아니라, 어떤 이벤트가 일어났는지를 팔로워에게 전달한다는 것이다. 이벤트/델마 모델을 사용함으로써 과한 정보들이 네트워크들을 통해 송수신되는 것을 방지할 수 있다는 게 장점이 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 브로커 하나가 다운되면서 3만 개의 메타데이터의 변화가 생겼을 때 3만개의 메타데이터를 복제해야하는 것은 큰 작업이다. 하지만 특정 브로커 하나가 다운되었다는 하나의 이벤트만 팔로워에게 넘겨주면 나머지는 팔로워가 자체적으로 3만개의 메타데이터를 변경을 가해주면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메타데이터 캐시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카의 클러스터의 &lt;u&gt;다양한 메타데이터 정보를 빠르게 접근할 수 있게 하기 위해 메모리에 현재 메타데이터 정보를 기록하는데 이를 &quot;메타데이터 캐시&quot;라고 한다.&lt;/u&gt; 이를 통해 카프카는 메타데이터 조회를 위해 항상 디스크에 접근하지 않고 빠르게 캐시 된 데이터에 접근할 수 있어 성능이 향상된다. 같은 맥락으로 클라이언트에서 메타데이터를 요구할 때 이 메타데이터 캐시를 이용하여 빠르게 응답을 줄 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;갱신 시점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팔로워는 Fetch 요청을 통해 리더의 메타데이터 로그를 따라잡는다고 위에서 얘기했는데, 이 Fetch 요청에는 팔로워가 현재 리더 로그의 어느 지점까지 복제 완료했는지를 표현하는 데이터를 포함하게 된다. 이를 통해 &lt;u&gt;Active Controller는 팔로워들이 각각 어느 지점까지 자신의 로그를 따라왔는지를 알 수 있고, 모든 노드가 복제에 성공한 최신 지점까지를 메타데이터 캐시에 적용해도 된다는 명령을 내릴 수 있다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 &lt;u&gt;클러스터 내 모든 노드가 복제에 성공한 최신 데이터가 저장된다는 점&lt;/u&gt;이다. 예를 들어 A(리더), B, C(팔로워)라는 노드가 클러스터에 참여하고 있다고 가정해 보자. A에는 메타데이터 1,2,3이 저장되어 있고, B가 1,2,3까지 복제에 성공했다. 그런데 C가 아직 1,2까지밖에 복제를 못했다고 했을 때 A, B의 메타데이터 캐시에 1,2,3이 로그가 적용된 메타데이터가 올라가게 되면 &lt;u&gt;클러스터 내의 메타데이터 간의 불일치가 발생&lt;/u&gt;하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;새로운 클린업 정책 = 스냅샷 정책&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 &lt;u&gt;메타데이터가 무한정 쌓이게 된다면 여러 가지 문제들이 발생&lt;/u&gt;하게 된다. 일단 메타데이터도 토픽으로써 디스크에 저장된다 했으므로 당연히 메타데이터가 계속 쌓이게 되면 &quot;&lt;u&gt;디스크 용량 부족&lt;/u&gt;&quot; 문제를 겪을 수 있다. 또한 새로운 노드가 클러스터에 참여하게 되면 현재까지 모든 메타데이터 로그를 복제해야 하므로 &quot;&lt;u&gt;동기화하는 시간이 증가&lt;/u&gt;&quot;하는 문제가 있을 수도 있다. 또 현재까지의 모든 로그를 팔로워에게 전달해야 하므로 &quot;&lt;u&gt;리더 역시 리소스를 낭비&lt;/u&gt;&quot;하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존 카프카에서는 이를 위해 토픽에 삭제 정책, 압축 정책 등이 있었으나,&lt;u&gt; 메타데이터가 이벤트/델타 모델을 사용하게 되면서 기존의 클린업 정책들을 사용할 수 없어 스냅샷이라는 새로운 정책이 등장&lt;/u&gt;하게 되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특정 시점이 되었을 때 &lt;u&gt;Active Controller는 현재 메모리에 올라가 있는 메타데이터 캐시를 스냅샷 형태로 변환해서 저장한다. 그리고 현시점 기준 이전의 메타데이터 로그를 삭제&lt;/u&gt;한다. 이렇게 하면 &lt;u&gt;새로운 노드가 참여하더라도 이전의 모든 메타데이터 로그를 넘겨주는 것이 아니라 스냅샷을 전달&lt;/u&gt;해주기만 하면 된다.&lt;/p&gt;</description>
      <category>Programming/Apache Kafka</category>
      <author>모종</author>
      <guid isPermaLink="true">https://monny.tistory.com/292</guid>
      <comments>https://monny.tistory.com/292#entry292comment</comments>
      <pubDate>Sun, 7 Jan 2024 17:04:56 +0900</pubDate>
    </item>
    <item>
      <title>KRaft 합의 알고리즘</title>
      <link>https://monny.tistory.com/291</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;zookeeper를 이용한 카프카 메타데이터 관리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;952&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d4a7tC/btsC35RjMja/veiXBpgQ084KMkhrQEPiSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d4a7tC/btsC35RjMja/veiXBpgQ084KMkhrQEPiSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d4a7tC/btsC35RjMja/veiXBpgQ084KMkhrQEPiSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd4a7tC%2FbtsC35RjMja%2FveiXBpgQ084KMkhrQEPiSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;711&quot; height=&quot;952&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;952&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;카프카 2.8 버전 이전에는 아래와 같은 메타데이터들을 관리하기 위해 zookeeper를 사용했었음&lt;/u&gt;.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-list=&quot;bullet&quot;&gt;클러스터 토폴로지 정보, 토픽 메타데이터, 컨트롤러 정보, 컨슈머 그룹 정보, ACLs 정보, 브로커 정보 등..&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Zookeeper를 통해 메타데이터를 관리했을 떄 발생하는 문제들&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;Zookeeper 자체에 이슈가 있다기보다는 주키퍼를 활용하여 메타데이터를 관리하는 카프카의 방식에 여러 가지 문제가 있었음.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주키퍼라서 생기는 문제들.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-list=&quot;bullet&quot;&gt;주키퍼는 카프카와 별개의 분산 시스템&lt;/li&gt;
&lt;li data-list=&quot;bullet&quot;&gt;관리자는 카프카를 배포하기 위해 두 개의 개별 분산 시스템을 관리하고 배포하는 방법을 배워야 한다.&lt;/li&gt;
&lt;li data-list=&quot;bullet&quot;&gt;SASL과 같은 설정이 카프카, 주키퍼 두 시스템에 모두 적용되어야 한다.&lt;/li&gt;
&lt;li data-list=&quot;bullet&quot;&gt;로컬에서 간단한 테스트를 해보고 싶더라도 주키퍼, 카프카 두 가지의 애플리케이션 모두 운영해야 한다.&lt;/li&gt;
&lt;li data-list=&quot;bullet&quot;&gt;주키퍼 Watch의 한계점&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메타데이터 복제 프로세스 문제&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-list=&quot;bullet&quot;&gt;컨트롤러는 메타데이터 변경 사항이 생기면 클러스터 내의 브로커들에게 변경된 내용을 전파(LeaderAndISr, UpdateMetadata 등) 하도록 되어있는데, 이 전송이 성공적으로 전달되지 않으면 브로커 간의 메타데이터 상태가 불일치하게 된다.&lt;/li&gt;
&lt;li data-list=&quot;bullet&quot;&gt;업데이트되지 않은 노드에게 클라이언트는 메타데이터를 요구할 수 있다. 이로 인해 클라이언트는 잘못된 정보를 받을 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Quorum 기반의 메타데이터 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카의 KIP문서를 보면 이를 &quot;post zookeeper&quot;라고 하면서 처음 소개를 하고 있음.&lt;u&gt; 카프카의 메타데이터 관리를 할 때 주키퍼를 활용하지 않고 카프카 자체적으로 메타데이터를 관리하도록 변경&lt;/u&gt;되었으며, 각 &lt;u&gt;클러스터 간의 Quorum을 만들고 참여하는 노드 사이에 합의 알고리즘을 이용하여 메타데이터를 관리&lt;/u&gt;함. 이 &lt;u&gt;합의 알고리즘으로 Raft 알고리즘을 사용하는데 Kafka 시스템에 좀 더 적합한 형태로 변화를 살짝 주었는데 이를 KRaft(Kafka Raft) 알고리즘&lt;/u&gt;이라 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sCxFm/btsC4egnUIE/EPJ9LnDBEHuMlgFktMWBJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sCxFm/btsC4egnUIE/EPJ9LnDBEHuMlgFktMWBJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sCxFm/btsC4egnUIE/EPJ9LnDBEHuMlgFktMWBJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsCxFm%2FbtsC4egnUIE%2FEPJ9LnDBEHuMlgFktMWBJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;641&quot; height=&quot;357&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Raft 합의 알고리즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;분산 시스템 환경에서 모든 노드가 동일한 상태를 유지하도록 하고, 일부 노드의 결함이 전체 시스템에 영향이 없도록 동작하기 위한 합의 알고리즘&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TMI로 Raft 알고리즘 이름의 유래가 재밌었는데 검색하다 보면 &quot;Raft&quot;라는 이름이 'reliable', 'replicated', 'redundant', 'fault-tolerant' 등의 단어의 조합된 것이 아닐까 하는 추측들이 있는데, 실제 이름을 짓는 과정에서 Raft 알고리즘의 특징인 log(통나무)라는 것을 이용하여 Paxos(Raft 이전에 자주 사용하던 합의 알고리즘, 그리스에 있는 섬이름임) 섬을 탈출한다에 집중하였고 Raft라는 이름이 등장하게 되었다고 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Raft에서 등장하는 용어들을 먼저 살펴보면 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Leader: 클러스터를 대표하는 노드. 모든 팔로워에게 명령을 전파한다.&lt;/li&gt;
&lt;li&gt;Follwer: 리더를 제외한 노드. 리더로부터 전파된 명령을 로컬에서 수행&lt;/li&gt;
&lt;li&gt;Candidate: 선거 기간 동안 리더가 될 자격을 얻은 노드. 선거에서 패하면 팔로워로 추락&lt;/li&gt;
&lt;li&gt;Term: &quot;제1대 대통령&quot;, &quot;제2대 대통령&quot;처럼 해당 선거가 몇 번째 선거인지를 나타내는 번호&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Raft 합의 알고리즘의 동작 과정은 아래의 사이트를 통해 확인해 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;https://thesecretlivesofdata.com/raft/&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KRaft 알고리즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KRaft 알고리즘은 Raft 알고리즘에 약간의 변형을 주어 만들었다고 했는데, &lt;u&gt;Raft 알고리즘에서 리더 선출과정을 통해 선정된 Leader 노드  는 Follwer 리더에게 로그를 전달(push)하도록 되어있다. 하지만 KRaft 알고리즘에서는 반대로 Follower가 Leader노드에게 변경된 로그를 요청(pull)하도록 되어있다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 변형을 준 이유로 카프카에서는 기존 시스템의 로그 레이어를 재사용하기 편하고, 리더의 과한 역할을 제거하고, 팔로워가 많아질수록 pull 방식이 적합하다고 판단하여서 팔로워가 리더에게 로그를 직접 요청하도록 수정했다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Programming/Apache Kafka</category>
      <category>캐</category>
      <author>모종</author>
      <guid isPermaLink="true">https://monny.tistory.com/291</guid>
      <comments>https://monny.tistory.com/291#entry291comment</comments>
      <pubDate>Sun, 7 Jan 2024 15:53:47 +0900</pubDate>
    </item>
    <item>
      <title>카프카 모니터링 - 메트릭</title>
      <link>https://monny.tistory.com/290</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;카프카 메트릭&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 코드를 보다 보면 메트릭을 수집해서 특정 클래스에서 계속 계산하는 모습을 볼 수 있습니다. 해당 클래스들은 MBean을 상속받아 작성되어 있어서 내부 메트릭을 JMX를 통해 얻을 수 있습니다. 브로커 뿐만 아니라 KRaft 같은 메타데이터, Producer, Consumer 등의 메트릭도 모두 JMX를 이용할 수 있습니다. 다만 메트릭에 접근하기 위해서는 몇 가지 설정이 필요합니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Java Management Extensions (JMX)는 Java 애플리케이션을 모니터링하고 관리하기 위한 기술.&lt;br&gt;JMX는 MBean (Managed Bean)이라는 관리 가능한 자바 오브젝트를 사용하는데 애플리케이션은 이 MBean을 통해 다양한 정보와 작동 상태를 노출하며, 관리자 또는 모니터링 도구는 JMX 인터페이스를 통해 이러한 정보에 접근할 수 있습니다.&lt;/blockquote&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;JMX 활성화&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;카프카의 실행 스크립트의 일부분을 보면 아래와 같습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# JMX settings
if [ -z &quot;$KAFKA_JMX_OPTS&quot; ]; then
&amp;nbsp;&amp;nbsp;KAFKA_JMX_OPTS=&quot;-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false&amp;nbsp;&amp;nbsp;-Dcom.sun.management.jmxremote.ssl=false &quot;
fi

# JMX port to use
if [&amp;nbsp;&amp;nbsp;$JMX_PORT ]; then
&amp;nbsp;&amp;nbsp;KAFKA_JMX_OPTS=&quot;$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT &quot;
&amp;nbsp;&amp;nbsp;if ! echo &quot;$KAFKA_JMX_OPTS&quot; | grep -qF -- '-Dcom.sun.management.jmxremote.rmi.port=' ; then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# If unset, set the RMI port to address issues with monitoring Kafka running in containers
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;KAFKA_JMX_OPTS=&quot;$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT&quot;
&amp;nbsp;&amp;nbsp;fi
fi&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;카프카를 실행할 때 JMX_PORT 옵션을 사용해야 합니다. 좀 더 디테일한 상세 설정이 필요하다면 KAFKA_JMX_OPTS를 사용하면 됩니다.&amp;nbsp;&lt;br&gt;도커를 사용하는 경우 이와 같은 설정을 하면 됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;version: &quot;3.9&quot;

services:
&amp;nbsp;&amp;nbsp;kafka-1:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;container_name: kafka-1
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;image: docker.io/bitnami/kafka:3.5
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ports:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 19092:9092
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- {{JMX포트}}:{{JMX포트}}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;environment:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
	&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# JMX
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- JMX_PORT={{JMX포트}}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- KAFKA_JMX_OPTS=-Dcom.sun.management.jmxremote -Djava.rmi.server.hostname={{호스트명}} -Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false&amp;nbsp;&amp;nbsp;-Dcom.sun.management.jmxremote.ssl=false&lt;/code&gt;&lt;/pre&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt; JMX 초기화과정&lt;/h3&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMdAk5/btsBjE0MdAL/0VxgUFx5ObNk27oCk1vpQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMdAk5/btsBjE0MdAL/0VxgUFx5ObNk27oCk1vpQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMdAk5/btsBjE0MdAL/0VxgUFx5ObNk27oCk1vpQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMdAk5%2FbtsBjE0MdAL%2F0VxgUFx5ObNk27oCk1vpQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;519&quot; height=&quot;254&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jmx, rmi 에 대해서 간단하게  알아보고 어떤 과정을 통해 실행되는지 흝어보면 도움이 될 것 같습니다. RMI(Remote Method Invocation)는 Java에서 원격 객체 간의 상호작용을 가능하게 하는 API입니다. rmi를 사용하면 로컬에서 네트워크를 통해 다른 JVM에 있는 객체의 메서드를 호출할 수 있습니다.&lt;br&gt;JMX, RMI는 아래와 같은 과정을 거쳐 최초 초기화됩니다.&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;kafka가 실행하면서 jmx agent를 같이 실행&lt;/li&gt;&lt;li&gt;jmx agent가 실행될 때 내부적으로 rmi server가 실행됨.&lt;/li&gt;&lt;li&gt;kafka는 자신의 MBean인터페이스를 상속받은 메트릭 클래스들을 jmx agent에 등록&lt;/li&gt;&lt;/ol&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;JMX Client 연결&amp;nbsp;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우 jmx port를 9999번으로 설정했고, rmi port를 9998번으로 설정하였습니다. 도커로 카프카를 실행했기에 컨테이너의 &quot;9999&quot; 포트를 호스트의 &quot;9999&quot; 포트로 매핑시켜 외부에 노출시켰습니다. 이후에 jmx client인 jconsole을 사용해 연결을 시도하니 계속 실패하였습니다.&lt;br&gt;이는 연결 과정을 알고나면 굉장히 어이없는 문제입니다. Jconsole이 jmx agent와 연결되는 과정은 아래와 같습니다.&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt; jconsole은 jmx 포트를 통해 jmx 서버에 연결을 시도&lt;/li&gt;&lt;li&gt;jmx 서버는 jconsole에게 rmi 포트 정보를 제공&lt;/li&gt;&lt;li&gt;jconsole은 전달받은 rmi 포트를 사용하여 jmx의 rmi 서버와 연결&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위 문제는 &lt;u&gt;jmx 포트 번호만 노출시킨다 해서 연결이 되는 게 아니라 rmi 포트도 노출&lt;/u&gt;시켜 주어야 해결이 가능합니다만&amp;nbsp;&lt;u&gt;rmi와 jmx를 동일한 포트를 사용하도록 설정도 가능&lt;/u&gt;합니다. 위에서 KAFKA_JMX_OPTS에 `com.sun.management.jmxremote.rmi.port` 프로퍼티를 설정하지 않는 경우 JMX_PORT를 이용해서 rmi port, jmx port를 설정하도록 돼있으니 이점 유의해서 사용하면 됩니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;Jconsole 실행&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;jconsole은 jdk에 이미 포함되어 있는 상태이기 때문에 자바가 이미 설치되어 있다면 터미널로 접속해서 &quot;jconsole&quot;을 입력해서 실행시켜 줄 수 있습니다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;465&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvLMrt/btsBfiE1uuF/ZTFGBj4nHQz2bKXZLb3nQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvLMrt/btsBfiE1uuF/ZTFGBj4nHQz2bKXZLb3nQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvLMrt/btsBfiE1uuF/ZTFGBj4nHQz2bKXZLb3nQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvLMrt%2FbtsBfiE1uuF%2FZTFGBj4nHQz2bKXZLb3nQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;465&quot; height=&quot;203&quot; data-origin-width=&quot;465&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jconsole이 실행되었다면 접속 정보를 입력해주면 되는데 주의할 점은 hostname입니다. 원래 기본값은 localhost인데 저의 경우 도커컴포즈 환경 + jmx expoter 같은 추가적인 세팅을 하다 보니 문제가 좀 있어서 `java.rmi.server.hostname`을 kafka-1로 설정해 주었습니다. 그래서 로컬 hosts 파일에 kafka-1 -&amp;gt; localhost로 등록해주어야 했습니다. 해당 프로퍼티를 사용하지 않은 분이라면 그냥 localhost로 접속시도하면 됩니다.&lt;br&gt;접속이 완료되면 이제 아래와 같이 메트릭을 보실수 있을 겁니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8Fits/btsBjrtIvA8/VkGaWiYhAGLDT9vOOhxCS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8Fits/btsBjrtIvA8/VkGaWiYhAGLDT9vOOhxCS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8Fits/btsBjrtIvA8/VkGaWiYhAGLDT9vOOhxCS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8Fits%2FbtsBjrtIvA8%2FVkGaWiYhAGLDT9vOOhxCS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;897&quot; height=&quot;672&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;</description>
      <category>Programming/Apache Kafka</category>
      <author>모종</author>
      <guid isPermaLink="true">https://monny.tistory.com/290</guid>
      <comments>https://monny.tistory.com/290#entry290comment</comments>
      <pubDate>Sat, 2 Dec 2023 16:25:29 +0900</pubDate>
    </item>
  </channel>
</rss>