<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>파즈의 공부 일기</title>
    <link>https://bepoz-study-diary.tistory.com/</link>
    <description>https://github.com/Be-poz/TIL</description>
    <language>ko</language>
    <pubDate>Sat, 30 May 2026 07:54:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Bepoz</managingEditor>
    <image>
      <title>파즈의 공부 일기</title>
      <url>https://tistory1.daumcdn.net/tistory/3327395/attach/ad0efec0ba28496db7e8b8a41ad40ad8</url>
      <link>https://bepoz-study-diary.tistory.com</link>
    </image>
    <item>
      <title>블로그를 이전하게 되었습니다.</title>
      <link>https://bepoz-study-diary.tistory.com/442</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요 이렇게 인사드리는 것은 처음인 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧지 않은 기간동안 tistory를 제 기술블로그 플랫폼으로 사용해왔었는데요, 이번에 velog로 이전하게 되어 이렇게 글을 작성하게 되었습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;velog로 이전하게된 이유는 큰 이유는 없고 마크다운 형식의 글 가독성 때문에 옮기게 되었습니다.  항상 마크다운 형식으로 글을 작성해왔었기 때문에 tistory를 사용하면서 글의 폭이 좁게 보이는 것에 대해 불만을 항상 가지고 있었는데요, 최근에도 동일한 생각을 가지게 되면서 마크다운 지원을 확실하게 하는 velog로 옮겨버리자고 결심하게 되었습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 블로그 url은 '&lt;a href=&quot;https://velog.io/@bepoz/posts&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@bepoz&lt;/a&gt;' 입니다.&amp;nbsp; &lt;br /&gt;애초에 블로그 포스팅 취지를 위해 기술 블로그 및 글 작성을 시작한 것이 아니라 기록을 남기기 위해 시작을 했고 그래서 블로그 명이 '공부일기' 라고 지었었던 것인데요, 아무래도 취준생 시절 한창 공부할 때 보다 포스팅의 빈도가 줄어들게 된 것 같습니다. 이전에는 사소한 것이라도 대충 남기고 넘어갔다면 이제는 확실하게 모르는 사소한 것 들만 정리하다 보니 그렇게 된 것 같습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잡설이 길었네요, 아무쪼록 지금까지 tistory의 제 기술블로그를 봐주셔서 감사합니다. velog에서 다시 뵙도록 하겠습니다.&lt;/p&gt;</description>
      <author>Bepoz</author>
      <guid isPermaLink="true">https://bepoz-study-diary.tistory.com/442</guid>
      <comments>https://bepoz-study-diary.tistory.com/442#entry442comment</comments>
      <pubDate>Thu, 3 Oct 2024 20:03:18 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Producer, Consumer 코드 사용법</title>
      <link>https://bepoz-study-diary.tistory.com/441</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;Kafka Producer, Consumer 코드 사용법&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka Client를 이용한 프로듀서&lt;/h2&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;final Properties configs = new Properties();
configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

final KafkaProducer&amp;lt;String, String&amp;gt; producer = new KafkaProducer(configs);
final String key = &quot;key5&quot;;
final String message = &quot;this is value11&quot;;
final ProducerRecord&amp;lt;String, String&amp;gt; producerRecord = new ProducerRecord&amp;lt;&amp;gt;(TOPIC_NAME, message);
producer.send(producerRecord);
producer.flush();
producer.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;org.apache.kafka:kafka-clients&lt;/code&gt; 의존성을 추가한 상태다.&lt;br /&gt;&lt;code&gt;ProducerRecord&lt;/code&gt;를 통해 데이터 레코드를 입력할 수 있는데 파티션 지정까지 가능하다. 존재하지 않은 파티션 번호를 입력하니 동작이 아예 멈췄다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;flush()&lt;/code&gt;를 하는 이유는 단순히 &lt;code&gt;send&lt;/code&gt; 메서드를 호출한다고 해서 바로 브로커에 데이터를 넣는 것이 아니라 프로듀서 내부적으로 배치 전송을 하기 위해서 버퍼 메모리 영역에 레코드들을 대기시킨 후 배치의 수 또는 일정시간이 지나면 전송을 하게 되는데 이 덕분에 카프카는 차별화된 전송 속도를 가지게 된다. 아무튼 버퍼에서 대기를 하기 때문에 바로 전송하기 위해 호출을 해준 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;close()&lt;/code&gt;는 애플리케이션을 종료하기 전에 프로듀서 인스턴스의 리소스들을 안전하게 종료하기 위함이다.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서는 데이터 레코드를 위에서 언급한 버퍼로 쌓기 전에 &lt;code&gt;Partitioner&lt;/code&gt;를 이용해 어떤 파티션에 전송될 것인지를 정한다. 그 후 &lt;code&gt;Accumulator&lt;/code&gt;에 쌓아두다가 sender가 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파티셔너는 별다른 설정이 없으면 &lt;code&gt;DefaultPartitioner&lt;/code&gt;가 파티셔너로 설정되어 동작하는데, 버전마다 이 동작 방식이 살짝 다르다. &lt;a href=&quot;https://www.confluent.io/ko-kr/blog/apache-kafka-producer-improvements-sticky-partitioner/&quot;&gt;https://www.confluent.io/ko-kr/blog/apache-kafka-producer-improvements-sticky-partitioner/&lt;/a&gt; 이 글에 따르면 key 값이 null인 경우 2.3 버전 이전에는 라운드 로빈 방식, 2.4 이후에는 스티키 파티셔닝 방식이 default 방식이라고 나온다. key 값이 존재할 때에는 버전 관계없이 hash 방식이 default이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class CustomPartitioner implements Partitioner {

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        if(keyBytes == null) {
            return 0;
        }

        if(&quot;bepoz&quot;.equals(key.toString())) {
            return 1;
        }

        final List&amp;lt;PartitionInfo&amp;gt; partitionInfos = cluster.partitionsForTopic(topic);
        return Utils.toPositive(Utils.murmur2(keyBytes)) % partitionInfos.size();
    }

    @Override
    public void close() {}

    @Override
    public void configure(Map&amp;lt;String, ?&amp;gt; configs) {}
}

-------

final Properties configs = new Properties();
configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

configs.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, CustomPartitioner.class);

final KafkaProducer&amp;lt;String, String&amp;gt; producer = new KafkaProducer(configs);
final String key = &quot;bepoz&quot;;
final String message = &quot;bepoz value2&quot;;
final ProducerRecord&amp;lt;String, String&amp;gt; producerRecord = new ProducerRecord&amp;lt;&amp;gt;(TOPIC_NAME, key, message);
producer.send(producerRecord);
producer.flush();
producer.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 &lt;code&gt;Partitioner&lt;/code&gt; 인터페이스를 구현하는 커스텀 파티셔너를 생성하고 프로퍼티에 추가하여 동작시켰다.&lt;br /&gt;리턴되는 int가 바로 데이터가 들어갈 파티션 번호에 해당된다. key가 &lt;code&gt;bepoz&lt;/code&gt; 라면 1번, key가 없다면 0번, key가 존재한다면 해시값을 이용해 들어가게끔 한 것이다.&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;Future&amp;lt;RecordMetadata&amp;gt; send = producer.send(producerRecord);
RecordMetadata recordMetadata = send.get();
System.out.println(&quot;recordMetadata = &quot; + recordMetadata);
//recordMetadata = ksy-topic-0630-0@3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;send&lt;/code&gt; 함수 호출 후에 Future 타입을 받고 이를 통해 응답결과를 동기적으로 알 수 있다.&lt;br /&gt;결과를 보면 0번째 파티션의 오프셋 3에 저장되었다는 것을 알 수가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 동기이기 때문에 빠른 전송에 방해가 될 수 있다. 이를 위해 Callback 클래스를 생성해서 레코드의 전송 결과를 비동기 적으로 받아볼 수가 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public class CustomProducerCallback implements Callback {

    @Override
    public void onCompletion(RecordMetadata metadata, Exception exception) {
        if (exception != null) {
            System.out.println(&quot;Failed to send message with exception: &quot; + exception);
        } else {
            System.out.println(&quot;Successfully sent message with metadata: &quot; + metadata);
        }
    }
}

---

producer.send(producerRecord, new CustomProducerCallback());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;send&lt;/code&gt; 메서드에 같이 콜백 클래스를 넣으면 된다.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka Client를 이용한 컨슈머&lt;/h2&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;final Properties configs = new Properties();
configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
configs.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;ksy-group&quot;);
configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;earliest&quot;);

final KafkaConsumer&amp;lt;String, String&amp;gt; consumer = new KafkaConsumer(configs);
consumer.subscribe(List.of(TOPIC_NAME));

while (true) {
    ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(1000));
    for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
        System.out.println(&quot;Received message: &quot; + record);
    }
}

/*

Received message: ConsumerRecord(topic = ksy-topic-0630, partition = 2, leaderEpoch = 0, offset = 4, CreateTime = 1719748493611, serialized key size = -1, serialized value size = 15, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = this is value10)
Received message: ConsumerRecord(topic = ksy-topic-0630, partition = 2, leaderEpoch = 0, offset = 5, CreateTime = 1719748621546, serialized key size = 4, serialized value size = 15, headers = RecordHeaders(headers = [], isReadOnly = false), key = key3, value = this is value12)
Received message: ConsumerRecord(topic = ksy-topic-0630, partition = 2, leaderEpoch = 0, offset = 6, CreateTime = 1719750368124, serialized key size = 5, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = bepoz, value = bepoz value)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;poll&lt;/code&gt; 메서드에 들어가는 시간은 브로커로부터 데이터를 가져올 때 컨슈머 버퍼에 데이터를 기다리기 위한 타임아웃 간격을 뜻한다. 위의 프로듀서가 넣어준 데이터를 그대로 읽기 위해 &lt;code&gt;auto.offset.reset&lt;/code&gt; 값을 &lt;code&gt;earliest&lt;/code&gt;로 설정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 기본값인 auto sync를 이용한 것이지만, 컨슈머 강제종료 발생과 같은 경우에 데이터가 중복으로 들어오거나 유실될 수 있다. 만약 중복이나 유실을 허용하지 않는 서비스라면 자동 커밋이 아닌 명시적으로 오프셋을 커밋해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;commitSync()&lt;/code&gt; 메서드를 호출하면 된다. 하지만 브로커에 커밋 요청을 하고 커밋이 정상적으로 처리되었는지 응답하기까지 기다려야하기 때문에 컨슈머의 처리량에 영향을 끼친다. 동기가 아닌 비동기를 이용한 &lt;code&gt;commitAsync()&lt;/code&gt; 메서드를 사용할 수도 있는데, 커밋 요청을 전송하고 응답이 오기 전까지 데이터 처리를 수행할 수 있다. 하지만 커밋 요청이 실패했을 경우 현재 처리 중인 데이터의 순서를 보장하지 않으며 데이터의 중복 처리가 발생할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;commitAsync(1)
commitAsync(2)
commitAsync(3)   1 성공
commitAsync(4)   2 실패
3 성공
4 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령 위와 같은 케이스에서 2빼고 1,2,4는 성공했다. 하지만 2는 실패했기 때문에 해당 커밋을 다시 보낸다. 그러고 라스트 오프셋이 2가 되기 때문에 결국에는 3,4의 데이터를 다시 컨슘해서 커밋을 하게될 것이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;final Properties configs = new Properties();
configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
configs.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;ksy-group3&quot;);
configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;earliest&quot;);
configs.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, &quot;false&quot;);

final KafkaConsumer&amp;lt;String, String&amp;gt; consumer = new KafkaConsumer(configs);
consumer.subscribe(List.of(TOPIC_NAME));

while (true) {
    ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(1000));
    for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
        System.out.println(&quot;Received message: &quot; + record);
    }
    consumer.commitAsync((offsets, exception) -&amp;gt; {
        if (exception != null) {
            System.out.println(&quot;Failed to commit offsets with exception: &quot; + exception);
        } else {
            System.out.println(&quot;Successfully committed offsets: &quot; + offsets);
        }
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 비동기 commit을 사용할 때에 콜백 함수를 정의해서 사용할 수도 있다.&lt;br /&gt;자동 커밋을 false로 두면 &lt;code&gt;max.poll.records&lt;/code&gt; 설정이 조금 다르게 동작하는 것 같다. 1개씩 읽어오던데 이것과 관련한 것은 조금 깊어질 것 같아서 찾아보지 않았다. 그리고 &lt;code&gt;max.poll.records&lt;/code&gt;를 10으로 설정하고 파티션 0,1,2에 각각 3,3,4개씩 들어있는 상황이라면 이 3,3,4를 한 번에 읽는 것은 또 아닌 것 같다. 상황에 따라 파티션을 나눠서 읽어들이는 것을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private static class RebalanceListener implements ConsumerRebalanceListener {
    @Override
    public void onPartitionsRevoked(Collection&amp;lt;TopicPartition&amp;gt; partitions) {
        System.out.println(&quot;Partitions are revoked.&quot;);
        consumer.commitSync(currentOffsets);
    }

    @Override
    public void onPartitionsAssigned(Collection&amp;lt;TopicPartition&amp;gt; partitions) {
        System.out.println(&quot;Partitions are assigned.&quot;);
    }
}

---

consumer.subscribe(List.of(TOPIC_NAME), new RebalanceListener());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ConsumerRebalanceListener&lt;/code&gt;를 이용해서 리밸런스 발생 시의 동작도 제어할 수 있다.&lt;br /&gt;&lt;code&gt;onPartitionsRevoked&lt;/code&gt;는 리밸런스가 시작되기 직전에 호출되는 메서드이고, &lt;code&gt;onPartitionsAssigned&lt;/code&gt;는 리밸런스가 끝난 뒤에 파티션이 할당 완료되면 호출되는 메서드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리밸런스 발생 시에 데이터 중복 처리를 막기 위해서 리밸런스 발생 직전에 커밋을 시켜준 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머를 토픽에 subscribe 하여 읽는 것 외에도 특정 토픽의 특정 파티션을 컨슈머에 명시적으로 할당하여 운영할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;consumer.assign(Collections.singleton(new TopicPartition(TOPIC_NAME, PARTITION_NUMBER)));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;code&gt;subscribe()&lt;/code&gt; 메서드가 아닌 &lt;code&gt;assign()&lt;/code&gt; 메서드를 사용해서 특정 토픽의 파티션을 할당시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;KafkaConsumer&lt;/code&gt; 클래스는 &lt;code&gt;wakeup()&lt;/code&gt;이라는 컨슈머를 안전하게 종료할 수 있는 메서드를 지원한다. 해당 메서드가 호출된 후 &lt;code&gt;poll()&lt;/code&gt; 메서드가 호출되면 &lt;code&gt;WakeupException&lt;/code&gt; 예외가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;try {
    while (true) {
        ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(10));
        for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
            System.out.println(&quot;Received message: &quot; + record);
        }
    }
} catch (WakeupException e) {
    System.out.println(&quot;Wakeup consumer.&quot;);
} finally {
    consumer.close();
}

---

static class ShutdownThread extends Thread {
    @Override
    public void run() {
        consumer.wakeup();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;poll()&lt;/code&gt;메서드를 통해 지속적으로 레코드들을 받아 처리하다가 &lt;code&gt;wakeup()&lt;/code&gt;메서드가 호출되면, 다음 &lt;code&gt;poll()&lt;/code&gt;메서드가 호출될 때 &lt;code&gt;WakeupException&lt;/code&gt; 예외가 발생한다. 이 &lt;code&gt;wakeup()&lt;/code&gt; 메서드는 자바에서 셧다운 훅을 구현하여 명시적으로 구현할 수 있다. 셧다운 훅이란 사용자 또는 운영체제로부터 종료 요청을 받으면 실행하는 스레드를 뜻한다. 셧다운 훅을 사용하여 &lt;code&gt;wakeup()&lt;/code&gt; 메서드를 호출하였다.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Kafka를 이용한 프로듀서&lt;/h2&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.kafka:spring-kafka:3.2.1'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카 의존성을 설치하면 kafka-client 까지 같이 들어온다.&lt;br /&gt;스프링 카프카 프로듀서는 &lt;code&gt;KafkaTemplate&lt;/code&gt; 이라는 클래스를 사용한다.&lt;br /&gt;스프링에서 제공하는 기본 템플릿을 사용하든가 아니면 &lt;code&gt;ProducerFactory&lt;/code&gt;를 사용해서 직접 템플릿을 생성하는 방법이 있다.&lt;/p&gt;
&lt;img src=&quot;https://github.com/user-attachments/assets/e15c6419-bc5f-43e3-805c-2f8570cb12ee&quot; alt=&quot;image&quot; width=&quot;578&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yaml 파일에 옵션을 설정하면 자동으로 오버라이드되어 설정된다. 카프카 클라이언트에서는 bootstrap-servers, key-serailizer, value-serializer를 선언하지 않으면 예외가 발생하지만 스프링 카프카에서는 localhost:9091, StringSerializer로 기본값이 세팅된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@SpringBootApplication
public class KafkaStudyApplication implements CommandLineRunner {

    private final static String TOPIC_NAME = &quot;ksy-topic-0630&quot;;

    @Autowired
    private KafkaTemplate&amp;lt;String, String&amp;gt; template;

    public static void main(String[] args) {
        SpringApplication.run(KafkaStudyApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        template.send(TOPIC_NAME, &quot;Hello, World!&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 yaml에 기입해주었고, 기본 템플릿을 주입받아 사용하였다. &lt;code&gt;send&lt;/code&gt; 메서드는 오버로드된 여러 시그니처들이 있다. 위의 코드에서는 토픽 명과 데이터만 넣었지만, 키, 파티션, 타임스탬프를 받을 수 있게끔 되어있고, &lt;code&gt;ProducerRecord&lt;/code&gt;를 받는 메서드도 있다. 만약 &lt;code&gt;Message&amp;lt;?&amp;gt;&lt;/code&gt; 파라미터가 들어간 메서드를 사용한다면 헤더의 값으로 &lt;code&gt;KafakHeaders.TOPIC&lt;/code&gt;,&lt;code&gt;KafkaHeaders.PARTITION&lt;/code&gt;와 같은 값을 사용하면 된다.&lt;/p&gt;
&lt;br /&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Configuration
public class CustomKafkaTemplateConfiguration {

    private final static String BOOTSTRAP_SERVERS =
            &quot;&quot;;
    @Bean
    public KafkaTemplate&amp;lt;String, String&amp;gt; customKafkaTemplate() {
        final Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

        final DefaultKafkaProducerFactory&amp;lt;String, String&amp;gt; factory = new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(props);
        return new KafkaTemplate&amp;lt;&amp;gt;(factory);
    }
}

---

@Autowired
private KafkaTemplate&amp;lt;String, String&amp;gt; customKafkaTemplate;

public static void main(String[] args) {
    SpringApplication.run(KafkaStudyApplication.class, args);
}

@Override
public void run(String... args) throws Exception {
    customKafkaTemplate.send(TOPIC_NAME, &quot;Hello, World!!!&quot;).handle((result, ex) -&amp;gt; {
        if (ex != null) {
            System.out.println(&quot;Failed to send message: &quot; + ex);
        } else {
            System.out.println(&quot;Sent message: &quot; + result);
        }
        return result;
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 어플리케이션 내에서 프로듀서를 여러개 사용하고 싶을 때 커스텀한 카프카템플릿을 사용하면 된다.&lt;br /&gt;위와 같이 팩토리 클래스를 이용해 템플릿을 만들고 빈 등록을 해주었다. 그리고 해당 템플릿을 주입받아 사용하였다. handle 메서드를 이용하여 성공, 실패에 대한 콜백을 지정하였다. 저 상태로 코드를 돌리면 전송하기 전에 어플리케이션이 종료가 되어 원활하게 토픽에 데이터가 안들어갈 수도 있으므로 테스트 용이니 뒤에 &lt;code&gt;Thread.sleep()&lt;/code&gt;이나 여타 다른 시간을 버는 코드를 추가하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 기본적으로 제공해주는 &lt;code&gt;KafkaTemplate&lt;/code&gt;은 yml 파일의 기본 설정을 따른다. 만약 이것도 설정되어 있지 않다면 진짜 default 값을 따른다. 위에서는 프로듀서를 여러개 사용하고 싶을 때라고 했지만 사실 세밀한 설정을 하고 싶으면 그냥 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class CustomKafkaProducerListener implements ProducerListener {
    @Override
    public void onSuccess(ProducerRecord producerRecord, RecordMetadata recordMetadata) {
      ...
    }

    @Override
    public void onError(ProducerRecord producerRecord, RecordMetadata recordMetadata, Exception exception) {
      ...
    }
}

----------------------------------------------------------------------

KafkaTemplate template = new KafkaTemplate&amp;lt;&amp;gt;(factory);
template.setProducerListener(new CustomKafkaProducerListener());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;send().handle()&lt;/code&gt;을 통해 성공/실패에 따른 비동기 처리를 할 수도 있지만 위와 같이 &lt;code&gt;ProducerListener&lt;/code&gt;를 구현하여 등록할 수도 있다. 여러 프로듀서에서 공통적으로 사용이 될 로직이면 리스너 클래스를 생성해두고 사용하는 식이 좋아 보인다. 프로듀서 개별적으로 성공/실패에 따른 처리 로직이 필요하다면 &lt;code&gt;handle()&lt;/code&gt;에서 처리하는 것이 좋아보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 &lt;code&gt;LoggingProducerListener&lt;/code&gt;라는 로깅용 리스너 클래스를 따로 지원해주기도 한다.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Kafka를 이용한 컨슈머&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카의 컨슈머는 기본 컨슈머를 2개의 타입으로 나누고 커밋을 7가지로 나누어 세분화되어있다.&lt;br /&gt;타입은 레코드 리스너(MessageListener)(SINGLE 이라고 말하기도 함)와 배치 리스너(BatchMessageListener)가 있다. 전자는 단 1개의 레코드를 처리하고, 후자는 &lt;code&gt;poll()&lt;/code&gt; 메서드로 리턴받은 &lt;code&gt;ComsumerRecords&lt;/code&gt; 처럼 한 번에 여러 개 레코드들을 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카에서는 사용자가 사용할 만한 커밋의 종류를 7가지로 세분화해놨고 커밋이라고 부르지 않고 'AckMode'라고 부른다. 프로듀서에서 사용하는 acks 옵션과는 다르다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AcksMode&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RECORD&lt;/td&gt;
&lt;td&gt;레코드 단위로 프로세싱 이후 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BATCH&lt;/td&gt;
&lt;td&gt;poll()메서드로 호출된 레코드가 모두 처리된 이후 커밋, default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIME&lt;/td&gt;
&lt;td&gt;특정 시간 이후 커밋, 시간 간격을 선언하는 AckTime 옵션 설정 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COUNT&lt;/td&gt;
&lt;td&gt;특정 개수만큼 처리된 이후에 커밋, 레코드 개수를 선언하는 AckCount 옵션 설정 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COUNT_TIME&lt;/td&gt;
&lt;td&gt;TIME, COUNT 옵션 중 맞는 조건이 하나라도 나올 경우 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MANUAL&lt;/td&gt;
&lt;td&gt;acknowledge() 메서드가 호출되면 다음번 poll() 때 커밋을 한다. 매번 acknowledge() 메서드를 호출하면 BATCH 옵션과 동일하게 동작한다. 이 옵션을 사용할 경우에는 AcknowledgingMessageListener 또는 BatchAcknowledgingMessageListener를 리스너로 사용해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MANUAL_IMMEDIATE&lt;/td&gt;
&lt;td&gt;acknowledge() 메서드를 호출한 즉시 커밋한다. 이 옵션을 사용할 경우에는 AcknowledgingMessageListener 또는 BatchAcknowledgingMessageListener를 리스너로 사용해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@KafkaListener(topics = TOPIC_NAME, groupId = &quot;ksy-test&quot;)
public void recordListener(ConsumerRecord&amp;lt;String, String&amp;gt; record) {
    System.out.println(&quot;Received message: &quot; + record);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 위와 같이 사용하면 된다(String message 이렇게도 사용하려면 가능하긴 함). &lt;code&gt;KafkaListener&lt;/code&gt; 어노테이션은 위의 사용법 이외에도 property 값을 따로 지정해주거나, 병렬 처리, 특정 토픽의 파티션만 읽게끔 하는 등의 설정을 할 수 있다. &lt;a href=&quot;https://docs.spring.io/spring-kafka/reference/kafka/receiving-messages/listener-annotation.html&quot;&gt;KafkaListener annotaion docs&lt;/a&gt; 에서 확인해보자.&lt;br /&gt;SINGLE의 경우 Listener 타입 Default이기 때문에 따로 지정해줄 필요가 없다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;spring.kafka.listener.type: BATCH

----------------------------------------------------------------------------

@KafkaListener(topics = TOPIC_NAME, groupId = &quot;ksy-test9&quot;)
public void batchListener(ConsumerRecords&amp;lt;String, String&amp;gt; records) {
    for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
        System.out.println(&quot;record = &quot; + record);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스너의 타입을 &lt;code&gt;BATCH&lt;/code&gt;로 설정하고 여러 레코드들을 한 번에 가져올 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수동 커밋을 사용하고 싶다면 &lt;code&gt;BatchAcknowledgingMessageListener&lt;/code&gt;, &lt;code&gt;BatchConsumerAwareMessageListener&lt;/code&gt; 를 사용하면 된다. 이 때, &lt;code&gt;AckMode&lt;/code&gt;는 &lt;code&gt;MANUAL&lt;/code&gt;, &lt;code&gt;MANUAL_IMMEDIATE&lt;/code&gt;로 설정하면된다.&lt;/p&gt;
&lt;br /&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Bean
public KafkaListenerContainerFactory&amp;lt;ConcurrentMessageListenerContainer&amp;lt;String, String&amp;gt;&amp;gt; customContainerFactory() {
    final Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;earliest&quot;);

    final DefaultKafkaConsumerFactory&amp;lt;String, String&amp;gt; cf = new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(props);
    final ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
    factory.setConsumerFactory(cf);
    factory.setBatchListener(true);
    return factory;
}


-------------------------------------------------------------------------

@KafkaListener(topics = TOPIC_NAME, groupId = &quot;custom-listener&quot;, containerFactory = &quot;customContainerFactory&quot;)
public void customListener(ConsumerRecords&amp;lt;String, String&amp;gt; records) {
    for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
        System.out.println(&quot;custom listener's record = &quot; + record);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 어플리케이션 내에서 여러개의 리스너를 사용해야 하는 상황이고 설정이 각기 다르다면 커스텀한 리스너 컨테이너를 사용하면 된다. 어노테이션에 위의 코드와 같이 커스텀하게 만들어준 팩토리를 지정해주면 된다. yml 파일에 &lt;code&gt;auto.offset.reset&lt;/code&gt; 설정을 하지 않았다면 default가 &lt;code&gt;latest&lt;/code&gt; 이기 때문에 팩토리 설정을 안해준 리스너로는 &lt;code&gt;latest&lt;/code&gt;로 동작을 하겠지만 커스텀하게 만들어준 곳에는 &lt;code&gt;earliest&lt;/code&gt;로 박아두었기 때문에 토픽에서 모든 레코드들을 읽어오는 방식으로 동작하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;factory.setBatchListener(true);&lt;/code&gt; 이 구문 때문에 listener의 타입이 BATCH가 되어서 배치로 읽어오게 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@KafkaListener(topics = TOPIC_NAME, groupId = &quot;original3&quot;, properties = &quot;auto.offset.reset=earliest&quot;)
public void batchListener(ConsumerRecord&amp;lt;String, String&amp;gt; record) {
    System.out.println(record);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위와 같이 따로 containerFactory를 사용하지 않은 상황에서 yml에 &lt;code&gt;spring.kafka.listener.type: SINGLE&lt;/code&gt;로 해두었다면 단건으로 읽어오게 될 것이다. 유의해야 할 점은 파라미터로 &lt;code&gt;ConsumerRecords&lt;/code&gt;로 두면 오류가 난다는 것이다. 배치로 읽어들이는 것이 아니기 때문! &lt;code&gt;ConsumerRecord&lt;/code&gt;로 두어야 한다.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Cloud Function, Stream을 이용한 프로듀서와 컨슈머&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;spring-kafka:3.2.1&lt;/code&gt;, &lt;code&gt;spring-cloud-stream:4.1.3&lt;/code&gt;, &lt;code&gt;spring-cloud-stream-binder-kafka:4.1.3&lt;/code&gt; 기준 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java의 functional interface인 &lt;code&gt;Consumer&lt;/code&gt;, &lt;code&gt;Function&lt;/code&gt;, &lt;code&gt;Supplier&lt;/code&gt;을 이용해서 kafka에서 데이터를 읽고 쓰는 것을 할 수가 있다(spring-cloud-function). 정확히는 Bean 등록하여 사용하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  application:
    name: kafka-study
  cloud:
    function:
      definition: testConsumer;testFunction
    stream:
      bindings:
        default:
          binder: kafka
          contentType: application/json
        testConsumer-in-0:
          binder: kafka
          destination: ksyTest
          contentType: application/json
          group: ksy-test-group3
          consumer:
            batch-mode: true
        testFunction-in-0:
          destination: ksyTest
          group: ksy-test-function
          consumer:
              batch-mode: true
        testFunction-out-0:
          destination: ksyTest-out

      kafka:
        binder:
          brokers: &quot;&quot;
        bindings:
          testConsumer-in-0:
              consumer:
                start-offset: earliest
                configuration:
                  key.deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer
                  value.deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer
          testFunction-in-0:
            consumer:
              start-offset: earliest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정은 위와 같이 &lt;code&gt;spring.cloud.stream.bindings.&amp;lt;functionName&amp;gt;+in/out+&amp;lt;index&amp;gt;&lt;/code&gt; 형태를 가지고 그 하위에 다른 설정들을 작성하는 식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;in과 out은 말 그대로 input, output의 줄임이고, input은 읽어오는 토픽에 대한 정보를, output에는 write하는 토픽에 대한 정보를 입력하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;spring.cloud.stream.bindings&lt;/code&gt;에 대한 설정 정보는 &lt;a href=&quot;https://docs.spring.io/spring-cloud-stream/reference/spring-cloud-stream/binding-properties.html&quot;&gt;해당 docs 페이지&lt;/a&gt;를 참고하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;spring.cloud.stream.kafka.binder&lt;/code&gt; 하위의 정보는 &lt;a href=&quot;https://docs.spring.io/spring-cloud-stream/reference/kafka/kafka-binder/config-options.html&quot;&gt;해당 docs 페이지&lt;/a&gt;를 참고하자&lt;br /&gt;&lt;code&gt;spring.cloud.stream.kafka.binder.consumer/producer&lt;/code&gt; 하위에 여러 옵션이 있는데 configuration 옵션도 있다. 이곳에 그냥 직접적으로 설정 값을 입력할 수도 있다. 위의 yaml에서는 &lt;code&gt;key.deserializer&lt;/code&gt;와 &lt;code&gt;value.deserializer&lt;/code&gt;를 따로 설정해두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-kafka를 사용할 때에 default deserializer가 StringDeserializer이지만, kafka binder를 사용할 때에는 내부적으로 kafka-client를 사용하기 때문에(&lt;a href=&quot;https://docs.spring.io/spring-cloud-stream/reference/kafka/kafka-binder/overview.html&quot;&gt;ref&lt;/a&gt;) default로 ByteArrayDeserializer를 사용하기 때문에 사실 설정할 필요가 없긴하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docs에 나와있는 것 처럼 여러 binding이 있어 한 번에 설정하기 위해 default 값을 설정하고 싶다면 &lt;code&gt;spring.cloud.stream.kafka.default.consumer.&amp;lt;property&amp;gt;=&amp;lt;value&amp;gt;.&lt;/code&gt; 이런 식으로 둘 수도 있지만,&lt;br /&gt;spring-kakfa를 이용할 때 처럼 &lt;code&gt;spring.kafka.consumer&lt;/code&gt; 하위에 두어도 설정은 된다.&lt;br /&gt;어찌되었던 간에 &lt;code&gt;org.apache.kafka.clients.consumer.ConsumerConfig&lt;/code&gt; 이쪽으로 설정 값 들이 들어가고 이것으로 연결을 맺기 때문으로 보인다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;@Bean
public Consumer&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; testConsumer() {
    return messages -&amp;gt; {
        for (String message : messages) {
            System.out.println(&quot;[testConsumer]message = &quot; + message);
        }
    };
}

@Bean
public Function&amp;lt;List&amp;lt;String&amp;gt;, List&amp;lt;String&amp;gt;&amp;gt; testFunction() {
    return messages -&amp;gt; {
        System.out.println(&quot;[testFunction] message = &quot; + messages);
        return messages;
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 자체는 위와 같이 함수명을 yaml에 선언했던 definition과 일치시키면 된다.&lt;br /&gt;위의 코드에서는 Function의 return을 List으로 두었는데 이렇게 하면 해당 내부 요소가 하나씩 데이터로 나가는게 아니라 말 그대로 List 타입으로 들어가게 되니깐 조심하자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REFERENCE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-kafka/reference/introduction.html&quot;&gt;https://docs.spring.io/spring-kafka/reference/introduction.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-stream/reference/index.html&quot;&gt;https://docs.spring.io/spring-cloud-stream/reference/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-function/reference/spring-cloud-function/introduction.html&quot;&gt;https://docs.spring.io/spring-cloud-function/reference/spring-cloud-function/introduction.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 아파치 카프카 by 최원영&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Infra</category>
      <author>Bepoz</author>
      <guid isPermaLink="true">https://bepoz-study-diary.tistory.com/441</guid>
      <comments>https://bepoz-study-diary.tistory.com/441#entry441comment</comments>
      <pubDate>Tue, 24 Sep 2024 22:41:19 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot3에서의 @Enumerated(EnumType.STRING) 문제</title>
      <link>https://bepoz-study-diary.tistory.com/440</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
  &lt;h1&gt;Spring Boot3에서의 @Enumerated(EnumType.STRING) 문제&lt;/h1&gt;
  &lt;p&gt;Spring Boot 3 부터는 Hibernate 6 버전을 default로 사용하고 여기서는 &lt;code&gt;@Enumerated(EnumType.STRING)&lt;/code&gt; 을 enum 필드에 붙여도 db에 enum 타입으로 들어간다. 따라서 추가적인 조치를 취해주어야 한다.  &lt;/p&gt;
  &lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Column(name = &amp;quot;enum_name&amp;quot;, nullable = false, columnDefinition = &amp;quot;varchar&amp;quot;)
  @Enumerated(EnumType.STRING)
  private EnumName enumName = EnumName.BEPOZ;&lt;/code&gt;&lt;/pre&gt;
  &lt;hr&gt;
&lt;/div&gt;</description>
      <category>Spring</category>
      <author>Bepoz</author>
      <guid isPermaLink="true">https://bepoz-study-diary.tistory.com/440</guid>
      <comments>https://bepoz-study-diary.tistory.com/440#entry440comment</comments>
      <pubDate>Wed, 10 Jul 2024 20:34:05 +0900</pubDate>
    </item>
    <item>
      <title>ElasticSearch index.refresh_interval 옵션의 -1 값에 대해</title>
      <link>https://bepoz-study-diary.tistory.com/439</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;ES index.refresh_interval 옵션의 -1 값에 대해&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;GET {index_name}/_settings&lt;/span&gt;&lt;span&gt;을 조회해보면 index의 설정이 나오는데, 이때 &lt;/span&gt;&lt;span&gt;refresh_interval&lt;/span&gt;&lt;span&gt; 값이 나오는 경우도 욌고 없는 경우도 있다. 없다면 기본값인 1s 으로 돌아가는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;ES는 near-realtime으로 돌아간다. 만약 문서를 색인했으면 검색에서 조회가 되기 위해서는 refresh가 되는 것을 기다려야 한다. 이 때 사용되는 옵션이&lt;/span&gt;&lt;span&gt;refresh_interval&lt;/span&gt;&lt;span&gt; 다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;refresh 작업 자체가 비싼 작업이기 때문에 색인 성능을 높이기 위해서는 이 값을 올리는 것이 좋다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 값을 -1로 둔다는 것은 refresh를 비활성화한다는 뜻이기 때문에 추가된 문서가 검색이 되지 않을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;ES는 색인된 문서를 translog에 저장하고 조건이 충족되면 이 데이터가 lucene 세그먼트에 커밋되고 이때 문서가 검색 가능 상태로 전환된다. 예로들면 translog의 크기가 너무 커지거나, 일정 시간이 지나거나, 노드가 재시작될 때 등의 경우 자동으로 커밋이 발생하여 refresh 작업 없이도 문서가 검색 가능 상태가 될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;어쨋든 높은 색인 성능이 필요해서 이 값을 -1로 두었다면 색인이 끝난 뒤 -1 해제를 해주거나 검색을 위해 refresh api를 따로 호출을 하던가 해야한다. &lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;REFERENCE&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://sematext.com/blog/elasticsearch-refresh-interval-vs-indexing-performance/&quot;&gt;https://sematext.com/blog/elasticsearch-refresh-interval-vs-indexing-performance/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://opster.com/guides/elasticsearch/glossary/elasticsearch-refresh-interval/&quot;&gt;https://opster.com/guides/elasticsearch/glossary/elasticsearch-refresh-interval/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Infra</category>
      <author>Bepoz</author>
      <guid isPermaLink="true">https://bepoz-study-diary.tistory.com/439</guid>
      <comments>https://bepoz-study-diary.tistory.com/439#entry439comment</comments>
      <pubDate>Mon, 1 Jul 2024 17:08:05 +0900</pubDate>
    </item>
    <item>
      <title>k8s secret properties 파일로 volume mount 하기</title>
      <link>https://bepoz-study-diary.tistory.com/438</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;k8s secret properties 파일로 volume mount 하기&lt;/h1&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Secret
metadata:
  name: bepoz-secret
  labels:
    app: bepoz-app
type: Opaque
data:
  name: YmVwb3o= # 'bepoz'의 base64 인코딩&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: bepoz-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: bepoz-app
  template:
    metadata:
      labels:
        app: bepoz-app
    spec:
      containers:
      - name: busybox
        image: busybox:latest
        command: [&quot;sleep&quot;, &quot;200&quot;]
        volumeMounts:
        - name: secret-volume
          mountPath: &quot;/etc/secret-volume&quot;
      volumes:
      - name: secret-volume
        secret:
          secretName: bepoz-secret&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 secret을 volume 등록하고 mount 할 때 위와 같이 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 등록하게되면 mountPath에 아래와 같이 key가 파일이름, 그리고 내부 내용이 value로 들어가게 된다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;/ # ls
bin    dev    etc    home   lib    lib64  proc   root   sys    tmp    usr    var
/ # cd /etc/secret-volume
/etc/secret-volume # ls
name
/etc/secret-volume # cat name
bepoz&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 secret 값을 properties 형식으로 저장을 해야만 하는 상황이 있었다.&lt;br /&gt;컨테이너의 COMMAND 명령어로 secret 들을 읽어서 properties 파일로 만들게끔도 해봤는데, 파드 내부에서 진행했을 때에는 되었는데 이상하게도 계속 오류가 발생했다. 그래서 아예 secret을 mount 할 때 properties로 들어가게끔 하는 방법을 택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법은 사실 간단하다. key 값이 파일명이 되므로 이 key 값에 파일명을 입력하고 value에는 properties 내용을 작성하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Secret
metadata:
  name: bepoz-secret
  labels:
    app: bepoz-app
type: Opaque
data:
  info.properties: bmFtZTprYW5nCmFnZToxMDA= &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 value 값은&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;name:bepoz
age:100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값을 그대로 인코딩한 것이다. 여기서 value에 따옴표가 들어가면 문제가 생길 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;/etc/secret-volume # cat info.properties
name:kang
age:100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pod에 들어가서 mountPath에서 해당 secret을 확인해보면 적절하게 properties 파일로 되어있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이런 경우에는 그냥 도커파일을 이용해서 이미지 빌드 시에 COPY를 이용해서 파일을 넣어주는게 더 속편할 것 같다는 생각이 든다.&lt;/p&gt;</description>
      <category>Infra</category>
      <author>Bepoz</author>
      <guid isPermaLink="true">https://bepoz-study-diary.tistory.com/438</guid>
      <comments>https://bepoz-study-diary.tistory.com/438#entry438comment</comments>
      <pubDate>Thu, 4 Apr 2024 02:06:19 +0900</pubDate>
    </item>
    <item>
      <title>OpenSearch Sink Connector 등록 설정</title>
      <link>https://bepoz-study-diary.tistory.com/437</link>
      <description>&lt;h1&gt;OpenSearch Sink Connector 등록 설정&lt;/h1&gt;
&lt;h2&gt;OpenSearch Sink Connector 란&lt;/h2&gt;
&lt;p&gt;말 그대로 open search를 위한 sink connector 입니다. 카프카 토픽의 내용을 바로 open search의 index로 인덱싱하기 위해서 사용합니다. &lt;/p&gt;
&lt;p&gt;elastic search에서 fork 되어서 나온 open search 라고 elastic search sink connector를 사용하면 동작하지 않습니다.&lt;br&gt;( 관련해서 es 직원이 답변해둔 &lt;a href=&quot;https://forum.confluent.io/t/elasticsearch-sink-connector-version-compatibility-table/4145&quot;&gt;링크&lt;/a&gt; )  &lt;/p&gt;
&lt;p&gt;es sink connector의 경우 confluent 사에서 공식적으로 plugin을 제공한다(ex. confluentinc/kafka-connect-elasticsearch:14.0.12). &lt;/p&gt;
&lt;p&gt;open search sink connector의 경우 찾아보기론 confluent의 공식 plugin은 없으며 aiven이라는 곳에서 제공하는 plugin이 있다. 일부 설정은 es sink connector와 다르지만(ex. es sink connector에는 &amp;#39;topic.index.map&amp;#39;의 설정으로 토픽명과 인덱스명 매핑이 가능하지만 open search sink에는 없다), 대부분의 설정은 같다.  &lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;사용하기 위해 필요한 사항&lt;/h2&gt;
&lt;p&gt;모든 커넥터들이 그렇지만 open search sink connector 또한 plugin이 설치되어있어야 한다.&lt;br&gt;사용 중인 커넥터의 &amp;#39;plugin.path&amp;#39; 설정에 필요한 plugin 내용이 존재해야 한다.&lt;br&gt;어떤 방식을 사용해도 상관없지만 나는 아래의 방식을 사용했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;wget https://github.com/Aiven-Open/opensearch-connector-for-apache-kafka/releases/download/v3.0.0/opensearch-connector-for-apache-kafka-3.0.0.tar

tar -xf opensearch-connector-for-apache-kafka-3.0.0.tar&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 plugin을 준비하고 커넥트 서버를 재시작한 후 커넥트 서버에 &amp;#39;/connector-plugins&amp;#39; get api 를 호출하였을 때 플러그인이 추가가 되어있으면 이제 준비가 다 된 것이다. &lt;/p&gt;
&lt;Br/&gt;

&lt;h2&gt;커넥터 등록 api 설정&lt;/h2&gt;
&lt;p&gt;다른 여러 커넥터 등록 때와 동일하게 &amp;#39;/{connector-name}/config&amp;#39; 의 put 요청을 날리면 됩니다.&lt;br&gt;내가 로컬에서 테스트할 때에 돌렸던 body 값은 아래와 같다.  &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &amp;quot;tasks.max&amp;quot;:1,
    &amp;quot;connector.class&amp;quot;:&amp;quot;io.aiven.kafka.connect.opensearch.OpensearchSinkConnector&amp;quot;,
    &amp;quot;topics&amp;quot;: &amp;quot;test-topic&amp;quot;,
    &amp;quot;connection.url&amp;quot;: &amp;quot;{opensearch url}&amp;quot;,
    &amp;quot;connection.username&amp;quot;: &amp;quot;${file:/etc/secret-volume/connection_info.properties:username}&amp;quot;,
    &amp;quot;connection.password&amp;quot;: &amp;quot;${file:/etc/secret-volume/connection_info.properties:password}&amp;quot;,
    &amp;quot;type.name&amp;quot;: &amp;quot;_doc&amp;quot;,
    &amp;quot;key.ignore&amp;quot;: &amp;quot;true&amp;quot;,
    &amp;quot;key.converter&amp;quot;: &amp;quot;org.apache.kafka.connect.storage.StringConverter&amp;quot;,
    &amp;quot;value.converter&amp;quot;: &amp;quot;org.apache.kafka.connect.json.JsonConverter&amp;quot;,
    &amp;quot;value.converter.schemas.enable&amp;quot;: &amp;quot;false&amp;quot;,
    &amp;quot;schema.ignore&amp;quot;: &amp;quot;true&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;configuration의 정보는 &lt;a href=&quot;https://docs.confluent.io/kafka-connectors/elasticsearch/current/configuration_options.html&quot;&gt;confluent es sink connector configuration 공식 문서&lt;/a&gt;와 open search sink connector를 제공한 &lt;a href=&quot;https://github.com/Aiven-Open/opensearch-connector-for-apache-kafka/blob/main/docs/opensearch-sink-connector-config-options.rst&quot;&gt;aiven의 공식 문서&lt;/a&gt;를 참고했다. &lt;/p&gt;
&lt;p&gt;위의 body 내용은 &amp;#39;test-topic&amp;#39; 이라는 토픽에서 컨슘하여 open search에 인덱싱하는 커넥터를 등록한 것이다.&lt;br&gt;&lt;del&gt;open search sink connector는 topic명 그대로 index 명으로 생성이 된다. 따라서 이것을 원하지 않는다면 alias를 이용하자.&lt;/del&gt;&lt;br&gt;-&amp;gt; 아래의 인덱스 명 변경하기에서 다른 방법을 적어둠.&lt;/p&gt;
&lt;p&gt;username과 password 같은 값 들은 하드코딩해서 그대로 입력하면 안된다. 왜냐하면 해당 커넥터의 config 조회를 하면 그대로 들어나기 때문이다. 따라서 properties 파일 안에 넣어두고 그것을 읽어오는 방식을 취해야 한다. &lt;a href=&quot;https://docs.confluent.io/platform/current/connect/security.html#fileconfigprovider&quot;&gt;confluent에서도 권장하는 방법이다.&lt;/a&gt; &lt;/p&gt;
&lt;p&gt;이를 사용하기 위해서는 커넥트 설정이나 커넥터 설정에 아래와 같은 설정이 추가되어있어야 한다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;config.providers=file
config.providers.file.class=org.apache.kafka.common.config.provider.FileConfigProvider&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;Transforms 이용하여 토픽 데이터 변경하기&lt;/h2&gt;
&lt;p&gt;Confluent사가 지원하는 Transforms 기능을 이용하여 토픽의 데이터를 조금 변형하여 인덱싱되게끔 할 수 있다. &lt;/p&gt;
&lt;p&gt;공식문서는 &lt;a href=&quot;https://docs.confluent.io/platform/current/connect/transforms/overview.html&quot;&gt;https://docs.confluent.io/platform/current/connect/transforms/overview.html&lt;/a&gt; 이며, 일부 기능의 경우에는 &lt;a href=&quot;https://www.confluent.io/hub/confluentinc/connect-transforms&quot;&gt;connect-transformations&lt;/a&gt; 이라는 plugin 설치가 추가로 필요합니다.&lt;/p&gt;
&lt;p&gt;공식문서에 워낙 자세하게 나와있어서 자세한 설명은 필요없을 것 같고 일부 자주 사용될 것 같은 기능만 간략하게 말하겠다.&lt;/p&gt;
&lt;p&gt;커넥터를 등록할 때 사용했던 api를 그대로 사용을 하고 body 값에 추가적인 설정을 하면 됩니다.  &lt;/p&gt;
&lt;h3&gt;제외하고 싶은 필드&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;{
    ...
    &amp;quot;transforms&amp;quot;: &amp;quot;DropField&amp;quot;,
    &amp;quot;transforms.DropField.type&amp;quot;: &amp;quot;org.apache.kafka.connect.transforms.ReplaceField$Value&amp;quot;,
    &amp;quot;transforms.DropField.exclude&amp;quot;: &amp;quot;{제외하고 싶은 필드}&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;transforms 에는 그냥 임의의 이름을 두고, type에는 어떤 transforms를 사용할 것인지를 적어두면 된다. 위의 예시에서는 ReplaceFields$Value 를 택했고 value를 replace 하게끔 지원해주는 type이다. &lt;/p&gt;
&lt;p&gt;그리고 exclude 로 버리고자 하는 필드를 입력한다. exclude는 ReplaceField 의 properties 값이고 이건 transforms 종류마다 다르다.  &lt;/p&gt;
&lt;h3&gt;카프카 헤더값 추가&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;{
    ...
    &amp;quot;transforms&amp;quot;: &amp;quot;InsertField&amp;quot;,
     &amp;quot;transforms.InsertField.type&amp;quot;: &amp;quot;org.apache.kafka.connect.transforms.InsertField$Value&amp;quot;,
     &amp;quot;transforms.InsertField.offset.field&amp;quot;: &amp;quot;{offset 정보가 들어갈 필드의 이름}&amp;quot;,
     &amp;quot;transforms.InsertField.partition.field&amp;quot;: &amp;quot;{patition 정보가 들어갈 필드의 이름}&amp;quot;,
     &amp;quot;transforms.InsertField.timestamp.field&amp;quot;: &amp;quot;{topic record가 생성된 시각정보가 들어갈 필드의 이름}&amp;quot;,
     &amp;quot;transforms.InsertField.topic.field&amp;quot;: &amp;quot;{topic의 이름이 들어갈 필드의 이름}&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;일반적으로 카프카 메타데이터의 값이 색인되지 않기 때문에 위의 값으로 색인을 시킬 수가 있다.&lt;br&gt;아예 새로운 필드를 추가할 수도 있긴 하지만 하드코딩된 값을 추가하는 것만 가능하고 다른 필드의 값을 빼서 넣는 형식은 불가능하다.  &lt;/p&gt;
&lt;p&gt;여기서 생성되는 timestamp.field는 long type으로 자동생성되기 때문에 date 타입을 원한다면 미리 템플릿 등록을 해두어야 한다.  &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;PUT _template/{index name}
{
  &amp;quot;index_patterns&amp;quot;: [
    &amp;quot;{index pattern}&amp;quot;
    ], 
  &amp;quot;mappings&amp;quot;: {
    &amp;quot;properties&amp;quot; : {
      &amp;quot;kafka&amp;quot; : {
        &amp;quot;properties&amp;quot; : {
          &amp;quot;kafkaCreateDateTime&amp;quot; : {
            &amp;quot;format&amp;quot; : &amp;quot;epoch_millis&amp;quot;,
            &amp;quot;type&amp;quot; : &amp;quot;date&amp;quot;
          }
        }
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;특정 조건에 맞을 때에만 record 색인하기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;{
    ...
    &amp;quot;transforms&amp;quot;: &amp;quot;FilterExample&amp;quot;,
    &amp;quot;transforms.FilterExample.type&amp;quot;: &amp;quot;io.confluent.connect.transforms.Filter$Value&amp;quot;,
    &amp;quot;transforms.FilterExample.filter.condition&amp;quot;: &amp;quot;$[?(@.updatedFields.price)]&amp;quot;,
    &amp;quot;transforms.FilterExample.filter.type&amp;quot;: &amp;quot;include&amp;quot;,
    &amp;quot;transforms.FilterExample.missing.or.null.behavior&amp;quot;: &amp;quot;exclude&amp;quot;,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;필터를 줘서 해당 조건에 맞는 record를 include 할지 exclude 할지 결정하는 transforms 이다.&lt;br&gt;condition에는 jsonPath 형식으로 들어가게 된다. jsonPath의 filter operator 문법을 이용해서 진행하면 된다.  &lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;일자별 index 생성&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;{
    ...
    &amp;quot;transforms&amp;quot;: &amp;quot;TimestampRouter&amp;quot;,    
    &amp;quot;transforms.TimestampRouter.type&amp;quot;: &amp;quot;org.apache.kafka.connect.transforms.TimestampRouter&amp;quot;,
    &amp;quot;transforms.TimestampRouter.topic.format&amp;quot;: &amp;quot;${topic}-${timestamp}&amp;quot;,
    &amp;quot;transforms.TimestampRouter.timestamp.format&amp;quot;: &amp;quot;yyyyMMdd&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 topic을 indexing 하는데 일자별로 index가 생기길 원한다면 위와 같이 진행하면 된다.  &lt;/p&gt;
&lt;h3&gt;여러 transforms를 한 번에 사용하기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;{
    ...
    &amp;quot;transforms&amp;quot;: &amp;quot;FilterExample, InsertField&amp;quot;,
    &amp;quot;transforms.FilterExample.type&amp;quot;: &amp;quot;io.confluent.connect.transforms.Filter$Value&amp;quot;,
    &amp;quot;transforms.FilterExample.filter.condition&amp;quot;: &amp;quot;$[?(@.updatedFields.displayPrice)]&amp;quot;,
    &amp;quot;transforms.FilterExample.filter.type&amp;quot;: &amp;quot;include&amp;quot;,
    &amp;quot;transforms.FilterExample.missing.or.null.behavior&amp;quot;: &amp;quot;exclude&amp;quot;,
    &amp;quot;transforms.InsertField.type&amp;quot;: &amp;quot;org.apache.kafka.connect.transforms.InsertField$Value&amp;quot;,
    &amp;quot;transforms.InsertField.offset.field&amp;quot;: &amp;quot;{offset 정보가 들어갈 필드의 이름}&amp;quot;,
    &amp;quot;transforms.InsertField.partition.field&amp;quot;: &amp;quot;{patition 정보가 들어갈 필드의 이름}&amp;quot;,
    &amp;quot;transforms.InsertField.timestamp.field&amp;quot;: &amp;quot;{topic record가 생성된 시각정보가 들어갈 필드의 이름}&amp;quot;,
    &amp;quot;transforms.InsertField.topic.field&amp;quot;: &amp;quot;{topic의 이름이 들어갈 필드의 이름}&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여러 transforms를 한 번에 사용할 때에는 그냥 위와 같이 transforms안에 여러개를 정의해두면 된다.&lt;br&gt;동작하는 순서는 정의한 순서대로 동작한다.&lt;br&gt;따라서 만약 특정 필드를 exclude 시켰거나 필드의 이름을 변경시켰는데 그 다음 transforms 단계에서 해당 필드를 이용하려고 한다면 오류가 발생할 수도 있다.  &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;필터를 변경할 때에는 PUT 요청으로 변경이 아니라 커넥터 삭제 후 재등록 해줘야 적용이되는 것을 확인할 수 있었음!&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;인덱스 명 변경하기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;{  
    &amp;quot;transforms&amp;quot;: &amp;quot;InsertField, SetTopic&amp;quot;,
    &amp;quot;transforms.InsertField.type&amp;quot;: &amp;quot;org.apache.kafka.connect.transforms.InsertField$Value&amp;quot;,
    &amp;quot;transforms.InsertField.static.field&amp;quot;: &amp;quot;indexName&amp;quot;,
    &amp;quot;transforms.InsertField.static.value&amp;quot;: &amp;quot;my-index&amp;quot;,
    &amp;quot;transforms.SetTopic.type&amp;quot;: &amp;quot;io.confluent.connect.transforms.ExtractTopic$Value&amp;quot;,
    &amp;quot;transforms.SetTopic.field&amp;quot;: &amp;quot;indexName&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;aiven opensearch sink conneector는 타겟이 되는 토픽 명으로 인덱스를 그대로 생성하게 되는데, ExtractTopic을 이용해서 필드 value를 인덱스명으로 둘 수 있다.&lt;br&gt;필드가 계속 바뀌면 새로운 인덱스가 계속해서 생성이 될 것이기 때문에 InsertField를 이용해서 필드명과 필드 value를 세팅해두고 이후에 ExtractTopic을 이용함으로써 인덱스명 변경을 할 수가 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;주의해야 할 점&lt;/h2&gt;
&lt;p&gt;상품과 같이 id를 key로 가지고 있는 topic을 consume 하여 indexing 하는 경우 오류가 발생할 수 있다. &lt;/p&gt;
&lt;p&gt;동일한 document 대상으로 여러 task가 계속해서 update를 치게되는 과정에서 version 어쩌고하면서 문제가 발생할 수 있습니다. 이 문제는 document에 lock이 걸리지 않고 낙관적 락을 사용하기 때문에 동시에 update 되는 경우 발생한다고 한다.&lt;br&gt;&lt;a href=&quot;https://discuss.elastic.co/t/version-conflict-409-question/311335&quot;&gt;관련링크1&lt;/a&gt;, &lt;a href=&quot;https://discuss.elastic.co/t/version-conflict-issue-while-updating-data-continously/344065&quot;&gt;관령링크2&lt;/a&gt;  &lt;/p&gt;
&lt;p&gt;이럴 떄에는 &amp;#39;key.ignore&amp;#39; configuration 값을 true로 주면 해결이 된다. true로 주면 &amp;#39;{토픽명}+{파티션 번호}+{오프셋 번호}&amp;#39; 의 형식으로 id를 가지게 된다. &lt;/p&gt;
&lt;Br/&gt;

&lt;h3&gt;Q&amp;amp;A&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;커넥터를 삭제 후 다시 붙였을 때 데이터 처리가 되는지?&lt;ol&gt;
&lt;li&gt;qwer 이라는 토픽에서 컨슘하는 qwer-test 라는 커넥터를 생성하게 되면 qwer이라는 인덱스에 record들이 인덱싱 됩니다. 이후 이 커넥터를 지우고 다시 qwer-test 를 등록해도 토픽에 컨슈머 그룹 id와 offset 정보가 있기 때문에 데이터를 다시 처리하지 않습니다. 그렇다면 동일하게 qwer 토픽을 바라보는데 커넥터 이름을 qwer-test2 라고 만들었을 경우, 결국 동일한 인덱스에 수행하는 것이기 때문에 내부적으로 실제로 인덱싱 하는지 아니면 그대로 update 치는지는 자세하지 않지만 결국 동일한 인덱스 내부 값이 남게 됩니다. 만약 새로운 이름을 가진 커넥터에다가 기존의 인덱스를 지운다면 다시 새롭게 인덱싱을 하게 될 것입니다. &lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;task rebalancing이 있는가?&lt;ol&gt;
&lt;li&gt;일반 vm을 사용하는 것 처럼 task rebalancing이 있고 동작합니다. consume 도중 pod를 죽이면 리밸런싱이 일어나고 pod가 살아났을 때에도 다시 리밸런싱이 일어납니다. data 유실은 확인했을 때 없었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;helm, deployment 또는 pod를 재생성 하는 경우 기존의 connector가 날라가는가?&lt;ol&gt;
&lt;li&gt;날라가지 않습니다. connector 정보는 커넥트의 config 토픽에 저장되기 때문에 따로 pv 등을 이용하지 않아도 날라가지 않습니다. &lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;REFERENCE&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/installation/configuration/connect/index.html#bootstrap-servers&quot;&gt;https://docs.confluent.io/platform/current/installation/configuration/connect/index.html#bootstrap-servers&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://aiven.io/docs/products/kafka/kafka-connect/howto/opensearch-sink&quot;&gt;https://aiven.io/docs/products/kafka/kafka-connect/howto/opensearch-sink&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/connect/transforms/overview.html&quot;&gt;https://docs.confluent.io/platform/current/connect/transforms/overview.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://discuss.elastic.co/t/version-conflict-409-question/311335&quot;&gt;https://discuss.elastic.co/t/version-conflict-409-question/311335&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://discuss.elastic.co/t/version-conflict-issue-while-updating-data-continously/344065&quot;&gt;https://discuss.elastic.co/t/version-conflict-issue-while-updating-data-continously/344065&lt;/a&gt;&lt;/p&gt;</description>
      <category>Infra</category>
      <author>Bepoz</author>
      <guid isPermaLink="true">https://bepoz-study-diary.tistory.com/437</guid>
      <comments>https://bepoz-study-diary.tistory.com/437#entry437comment</comments>
      <pubDate>Wed, 3 Apr 2024 22:21:19 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] JPA Auditing에서 OffsetDateTime 사용하기</title>
      <link>https://bepoz-study-diary.tistory.com/436</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;JPA Auditing에서 OffsetDateTime 사용하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@EnableJpaAuditing&lt;/code&gt;을 사용한 jpa의 auditing에서 &lt;code&gt;@CreateDate&lt;/code&gt; 과 같은 Date 관련 기능은 기본적으로 LocalDateTime 타입이 할당된다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;/**
 * Default {@link DateTimeProvider} simply creating new {@link LocalDateTime} instances for each method call.
 *
 * @author Oliver Gierke
 * @author Christoph Strobl
 * @since 1.5
 */
public enum CurrentDateTimeProvider implements DateTimeProvider {

    INSTANCE;

    /*
     * (non-Javadoc)
     * @see org.springframework.data.auditing.DateTimeProvider#getNow()
     */
    @Override
    public Optional&amp;lt;TemporalAccessor&amp;gt; getNow() {
        return Optional.of(LocalDateTime.now());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;default로 위의 &lt;code&gt;getNow&lt;/code&gt; 메서드가 실행이 되는데 다른 세부 타입의 TemporalAccessor을 이용하고 싶다면 그냥 새롭게 빈 등록을 해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
public class DateTimeProviderConfig {

    @Bean(&quot;offSetDateTimeProvider&quot;)
    public DateTimeProvider dateTimeProvider() {
        return () -&amp;gt; Optional.of(OffsetDateTime.now());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DateTimeProvider&lt;/code&gt; 을 구현한 빈을 등록한 후,&lt;br /&gt;&lt;code&gt;@EnableJpaAuditing(dateTimeProviderRef = &quot;offSetDateTimeProvider&quot;)&lt;/code&gt; 좌측과 같이 넣어주면 완료~!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;</description>
      <category>Spring</category>
      <author>Bepoz</author>
      <guid isPermaLink="true">https://bepoz-study-diary.tistory.com/436</guid>
      <comments>https://bepoz-study-diary.tistory.com/436#entry436comment</comments>
      <pubDate>Thu, 4 Jan 2024 18:10:14 +0900</pubDate>
    </item>
    <item>
      <title>MongoDB 특정 필드만 가져오게끔 하는 projection</title>
      <link>https://bepoz-study-diary.tistory.com/435</link>
      <description>&lt;div class=&quot;markdowno-body&quot;&gt;
&lt;h1&gt;MongoDB 특정 필드만 가져오게끔 하는 projection&lt;/h1&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;몽고 DB에서 document를 조회할 때 매치되는 doc의 모든 필드를 가져오지만 projection을 이용해 특정 필드만 가져올 수가 있다. 필드 수가 많은 collection에서 내가 원하는 데이터의 특정 필드값만 조회하고 싶은데, 전체 필드를 가져오는 것은 굉장히 비효율 적일 것이다. 그럴 때에 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Person {
    @Id
    private String id;
    private Name name; //Name 클래스는 firstName과 lastName 이렇게 2개의 필드가 존재
    @Positive
    private int age;
    @CreatedDate
    private LocalDateTime createdAt;
    @LastModifiedDate
    private LocalDateTime modifiedAt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사용하려는 Person collection의 정보는 위와 같다.  &lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;MongoDB Compass에서의 사용&lt;/h2&gt;
&lt;img width=&quot;465&quot; alt=&quot;image&quot; src=&quot;https://github.com/Be-poz/TIL/assets/45073750/21e33128-b586-4698-8cbf-bd9bd4765d14&quot;&gt;

&lt;p&gt;위와 같이 2건의 데이터를 집어넣은 상태이다.&lt;br&gt;이 상황에서 &lt;code&gt;age&lt;/code&gt;가 20인 데이터를 검색하면 딱 저 데이터들이 리턴될 것이다. 하지만 내가 원하는 값은 &lt;code&gt;name&lt;/code&gt;만을 원한다면 project를 이용하면 된다.  &lt;/p&gt;
&lt;p&gt;아래는 mongoDB Compass에서 project를 사용했을 때의 결과이다.  &lt;/p&gt;
&lt;img width=&quot;371&quot; alt=&quot;image&quot; src=&quot;https://github.com/Be-poz/TIL/assets/45073750/47efcf42-de63-4209-8ec5-b4d4ed5e8168&quot;&gt;

&lt;p&gt;원하는 필드에 1을 기입하면 출력이 된다. Name은 Object 타입이고 내부에 여러 필드가 있다.  &lt;/p&gt;
&lt;img width=&quot;462&quot; alt=&quot;image&quot; src=&quot;https://github.com/Be-poz/TIL/assets/45073750/ece5e18a-20c4-45e6-84ee-54bb8389d3e4&quot;&gt;

&lt;p&gt;이렇게 상세 필드를 기입하면 해당 필드만 뽑아올 수가 있다.  &lt;/p&gt;
&lt;img width=&quot;400&quot; alt=&quot;image&quot; src=&quot;https://github.com/Be-poz/TIL/assets/45073750/d1af1194-94b8-44e2-9d26-bfc5de1f67ea&quot;&gt;

&lt;p&gt;이번에는 아예 0으로 두었더니 해당 필드만 빼고 가져온 것을 확인할 수가 있었다.  &lt;/p&gt;
&lt;p&gt;1로 두었을 때에는 해당 필드만 가져오지만 0으로 두면 해당 필드를 제외하고 가져오게 된다.  &lt;/p&gt;
&lt;img width=&quot;443&quot; alt=&quot;image&quot; src=&quot;https://github.com/Be-poz/TIL/assets/45073750/6b1e72e9-b783-4499-abc1-1a609fc5c12d&quot;&gt;

&lt;p&gt;1을 사용하게 되면 해당 필드말고도 &lt;code&gt;_id&lt;/code&gt; 값을 가져왔었는데 이 또한 0으로 명시해서 가져오지 않을 수가 있다.  &lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;@Query에서의 사용&lt;/h2&gt;
&lt;p&gt;이번에는 &lt;code&gt;@Query&lt;/code&gt; 어노테이션을 사용하여 projections을 사용해보겠다.  &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface PersonRepository extends MongoRepository&amp;lt;Person, String&amp;gt;, PersonRepositoryCustom {

    @Query(value = &amp;quot;{&amp;#39;age&amp;#39;: 20}&amp;quot;, fields = &amp;quot;{&amp;#39;name&amp;#39;: 1, &amp;#39;age&amp;#39;: 1}&amp;quot;)
    List&amp;lt;Person&amp;gt; findAllWithProjections();
}

---------------------------------------------------------------------------------

@GetMapping(&amp;quot;/projections&amp;quot;)
public List&amp;lt;Person&amp;gt; getPersonWithProjections() {
    return personRepository.findAllWithProjections();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
    {
        &amp;quot;id&amp;quot;: &amp;quot;64d8cb154360a130e3d5e422&amp;quot;,
        &amp;quot;name&amp;quot;: {
            &amp;quot;name&amp;quot;: &amp;quot;negative Dont&amp;quot;
        },
        &amp;quot;age&amp;quot;: 20,
        &amp;quot;createdAt&amp;quot;: null,
        &amp;quot;modifiedAt&amp;quot;: null
    },
    {
        &amp;quot;id&amp;quot;: &amp;quot;64d8cb31f7736e129cf1fb9b&amp;quot;,
        &amp;quot;name&amp;quot;: {
            &amp;quot;name&amp;quot;: &amp;quot;negative Dont&amp;quot;
        },
        &amp;quot;age&amp;quot;: 20,
        &amp;quot;createdAt&amp;quot;: null,
        &amp;quot;modifiedAt&amp;quot;: null
    }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;조회하면 필요한 값만 들고오는 것을 확인할 수 있다.&lt;br&gt; &lt;code&gt;fields = &amp;quot;{&amp;#39;name&amp;#39;: 1, &amp;#39;_id&amp;#39;: 0}&amp;quot;&lt;/code&gt; 이런 식으로 변경했을 때에도 적절한 response를 돌려준다.  &lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;MongoTemplate과 Query에서의 사용&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@PostMapping(&amp;quot;/projections&amp;quot;)
public List&amp;lt;Person&amp;gt; getPersonWithProjections(@RequestBody List&amp;lt;String&amp;gt; projections) {
    Query query = new Query();
    for (String projection : projections) {
        query.fields().include(projection);
    }
    return mongoTemplate.find(query, Person.class);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 &lt;code&gt;fields().include()&lt;/code&gt; 메서드를 이용할 수 있다. &lt;code&gt;exclude()&lt;/code&gt; 를 통해 위에서 0을 설정한 결과값을 받을 수도 있다.  &lt;/p&gt;
&lt;img width=&quot;499&quot; alt=&quot;image&quot; src=&quot;https://github.com/Be-poz/TIL/assets/45073750/699efec1-ac8f-4003-bf94-ee045d22e8c6&quot;&gt;

&lt;p&gt;include의 설명을 보면 &lt;code&gt;projection&lt;/code&gt;의 역할인 것을 확인할 수가 있다.  &lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;REFERENCE&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/&quot;&gt;https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Spring</category>
      <author>Bepoz</author>
      <guid isPermaLink="true">https://bepoz-study-diary.tistory.com/435</guid>
      <comments>https://bepoz-study-diary.tistory.com/435#entry435comment</comments>
      <pubDate>Sun, 13 Aug 2023 23:30:47 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Filter와 server.compression 설정을 통한 api 압축</title>
      <link>https://bepoz-study-diary.tistory.com/434</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;Filter와 server.compression 설정을 통한 api 압축&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API를 압축해서 return 하려고 한다. 먼저 가장 대표적인 &lt;code&gt;server.compression&lt;/code&gt; 설정을 알아보고 사용해보고 이후 Filter를 이용하여 조금 더 응용해보려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;server.compression 설정을 통한 api 압축&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 종류&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;server:
  compression:
    enabled:  
    min-response-size:
    mime-types:
    excluded-user-agents:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;server.compression&lt;/code&gt; 설정에는 위의 4개 항목이 있다. 단순히 yaml 파일에 기입을 해두면 동작한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;enabled&lt;/code&gt;: 압축 여부
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;default: false&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min-response-size&lt;/code&gt;: 압축을 수행할 최소 용량
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;default: 2KB&lt;/li&gt;
&lt;li&gt;아래에서 언급할 것이지만 현재 http에서는 의미가 없는 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mime-types&lt;/code&gt;: 압축을 수행할 MIME 타입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;default: text/html, text/xml, text/plain, text/css, text/javascript, application/javascript, application/json, application/xml&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;excluded-user-agents&lt;/code&gt;: 압축을 하지않을 user agents 목록&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;압축 전 후 데이터 용량 확인&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/compress&quot;)
public class CompressController {

    @GetMapping
    public List&amp;lt;PersonDto&amp;gt; create() {
        final List&amp;lt;PersonDto&amp;gt; dtos = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt; 100000; i++) {
            dtos.add(new PersonDto(&quot;name&quot; + i, i));
        }

        return dtos;
    }

    @Getter
    @AllArgsConstructor
    class PersonDto {
        private String name;
        private int age;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 api를 가지고 압축 전 후 용량을 확인해보았다.&lt;br /&gt;압축하길 바란다면 헤더에 &lt;code&gt;Accept-Encoding: gzip&lt;/code&gt; 이 추가되어야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;압축 X : 3.13MB&lt;/li&gt;
&lt;li&gt;압축 O: 471KB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스트맨을 사용해서 압축을 요청하면 포스트맨에서 response를 바로 압축해제 하여 보여주기 때문에 표시되어있는 용량으로는 압축된 것을 확인할 수 없으니 curl 요청을 해서 보는 방법으로 진행하였다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;curl -v --location 'localhost:8000/compress' \
--header 'Accept-Encoding: gzip'&amp;gt; 1

ls -alh 1&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;min-response-size 설정 동작 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;min-response-size&lt;/code&gt; 설정을 해두면 헤더의 &lt;code&gt;Content-Length&lt;/code&gt; 값과 비교하여 동작을 할 것인지 동작을 하지 않을 것인지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축 설정과 관련한 클래스는 &lt;code&gt;CompressionConfig&lt;/code&gt; 클래스인데 해당 클래스의 useCompression 메서드에 해당 내용이 나와있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public boolean useCompression(Request request, Response response) {
        ...

        // If force mode, the length and MIME type checks are skipped
        if (compressionLevel != 2) {
            // Check if the response is of sufficient length to trigger the compression
            long contentLength = response.getContentLengthLong();
            if (contentLength != -1 &amp;amp;&amp;amp; contentLength &amp;lt; compressionMinSize) {
                return false;
            }

            // Check for compatible MIME-TYPE
            String[] compressibleMimeTypes = getCompressibleMimeTypes();
            if (compressibleMimeTypes != null &amp;amp;&amp;amp;
                    !startsWithStringArray(compressibleMimeTypes, response.getContentType())) {
                return false;
            }
        }
              ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;contentLength가 설정값보다 작으면 압축 X, 설정값보다 크거나 contentLength 값이 -1이면 MIME 타입으로 체크한다고 나와있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재의 HTTP는 &lt;code&gt;Content-Length&lt;/code&gt; 정보가 response 헤더에 포함되어있지 않고 &lt;code&gt;Transfer-Encoding: chunked&lt;/code&gt; 값으로 대처하기 때문에 &lt;code&gt;Content-Length&lt;/code&gt; 값이 -1 으로 취급된다. 따라서 &lt;code&gt;min-response-size&lt;/code&gt; 설정은 현재로서는 의미가 없다.&lt;/p&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;mime-types 설정 동작 확인&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;  @GetMapping(path = &quot;/text&quot;)
  public String create2() {
      StringBuilder result = new StringBuilder();
      for (int i = 0; i &amp;lt; 100000; i++) {
          result.append(&quot;name&quot; + i);
      }
      return result.toString();
  }

---

server:
  compression:
    enabled: true
    mime-types: text/plain&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;mime-types&lt;/code&gt; 설정을 &lt;code&gt;text/plain&lt;/code&gt;으로 설정해주었고 확인을 위해 새로운 api를 추가해주었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Accept: application/json 으로 api 호출&lt;/h4&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;curl -v --location 'localhost:8000/compress' \
--header 'Accept-Encoding: gzip' \
--header 'Accept: application/json' &amp;gt; 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 맨 처음 추가해준 api를 압축요청을 한채로 요청을 해보았으나 압축을 하지 않았을 때의 용량인 3.1MB를 돌려받았다.&lt;br /&gt;압축이 되지 않은 것을 알 수가 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Accept: text/plain 으로 api 호출&lt;/h4&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;curl -v --location 'localhost:8000/compress/text' \
--header 'Accept-Encoding: gzip' \
--header 'Accept: text/plain' &amp;gt; 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 새롭게 추가한 api에 &lt;code&gt;text/plain&lt;/code&gt; 으로 압축요청을 하니 218KB를 반환했다.&lt;br /&gt;압축을 요청하지 않은채로 호출을 해보니 868KB를 반환받았다. 압축이 된 것을 확인할 수가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;mime-types&lt;/code&gt; 설정은 의미가 있다는 것을 확인할 수가 있었다.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Filter를 통한 api 압축&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;server.compression&lt;/code&gt; 설정은 특정 end point만 압축을 하는 기능을 지원하지 않는다.&lt;br /&gt;Filter를 이용해서 이것을 구현해보고자 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@WebFilter(&quot;/compress&quot;)
@Component
public class CompressFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // Response 객체가 HttpServletResponse 형인지 확인
        if (response instanceof HttpServletResponse) {
            // 압축하기 위한 ByteArrayOutputStream 생성
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);

            // 압축된 데이터를 HttpServletResponse로 전송할 수 있도록 필터 체인 내부에서 사용되는 응답 객체를 변경
            HttpServletResponse httpResponse = (HttpServletResponse) response;

            CompressedResponseWrapper compressedResponseWrapper = new CompressedResponseWrapper(httpResponse, gzipOutputStream);

            // Filter 체인을 통해 컨트롤러의 응답 처리
            chain.doFilter(request, compressedResponseWrapper);

            // 압축 스트림 닫기
            gzipOutputStream.close();

            // Content-Encoding 설정
            byte[] compressedData = byteArrayOutputStream.toByteArray();
            httpResponse.setHeader(&quot;Content-Encoding&quot;, &quot;gzip&quot;);

            // 압축된 데이터를 실제 응답에 기록
            response.getOutputStream().write(compressedData);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class CompressedResponseWrapper extends HttpServletResponseWrapper {

    private final GZIPOutputStream gzipOutputStream;

    public CompressedResponseWrapper(HttpServletResponse response, GZIPOutputStream gzipOutputStream) {
        super(response);
        this.gzipOutputStream = gzipOutputStream;
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new GzipServletOutputStream(gzipOutputStream);
    }

    @Override
    public void setContentLength(int len) {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class GzipServletOutputStream extends ServletOutputStream {

    private final GZIPOutputStream gzipOutputStream;

    public GzipServletOutputStream(GZIPOutputStream gzipOutputStream) {
        this.gzipOutputStream = gzipOutputStream;
    }

    @Override
    public void write(int b) throws IOException {
        gzipOutputStream.write(b);
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setWriteListener(WriteListener listener) {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/compress&lt;/code&gt; url에만 해당 필터를 적용시킨다. &lt;code&gt;server.compression&lt;/code&gt; 설정을 하지 않았다는 가정 하에 진행을 한 것이고 만약 해당 설정을 하고 있다면 2중으로 압축하려하면서 오류가 날 수도 있으니 헤더에 &lt;code&gt;Accept-Encoding: gzip&lt;/code&gt; 있는지 확인하고 압축 흐름을 타지않고 그냥 &lt;code&gt;chain.doFilter(request, response)&lt;/code&gt;만 호출하는 방식의 코드를 추가하면 될 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REFERENCE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.springcloud.io/post/2022-05/resttemplate-gzip/#gsc.tab=0&quot;&gt;https://www.springcloud.io/post/2022-05/resttemplate-gzip/#gsc.tab=0&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Spring</category>
      <author>Bepoz</author>
      <guid isPermaLink="true">https://bepoz-study-diary.tistory.com/434</guid>
      <comments>https://bepoz-study-diary.tistory.com/434#entry434comment</comments>
      <pubDate>Sat, 29 Jul 2023 16:19:29 +0900</pubDate>
    </item>
    <item>
      <title>여러 파드 로그 조회 명령어 Stern</title>
      <link>https://bepoz-study-diary.tistory.com/433</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;여러 파드 로그 조회 명령어 Stern&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;k logs {pod-name}&lt;/code&gt; 으로 파드를 조회할 때에 1개의 파드만 조회가능하다. 만약 replicas가 여러개라면 1개의 파드를 보는 것으로는 제대로된 로그파악이 되지 않는다.  &lt;/p&gt;
&lt;p&gt;&lt;code&gt;k logs -l app=bepoz&lt;/code&gt; 이렇게 레이블이 &lt;code&gt;app=bepoz&lt;/code&gt;인 것 들을 한 번에 조회할 수도 있긴하지만 &lt;code&gt;-f&lt;/code&gt; 옵션으로 지속적으로 로그변경을 확인하고 싶을 때에는 최대 5개의 파드까지만 가능하기 때문에 파드 수가 많다면 &lt;code&gt;k logs -f -l app-bepoz&lt;/code&gt; 이렇게 사용이 어렵다.  &lt;/p&gt;
&lt;p&gt;명령어 stern은 가능하다. stern은 쿠버네티스 클러스터의 여러 파드와 여러 컨테이너를 &lt;code&gt;tail&lt;/code&gt; 할 수 있게끔 해준다.  &lt;/p&gt;
&lt;p&gt;&lt;code&gt;brew install stern&lt;/code&gt; 으로 stern을 설치한다.  &lt;/p&gt;
&lt;p&gt;기본적으로 &lt;code&gt;stern pod-query [flags]&lt;/code&gt;의 형태로 명령어를 사용한다.  &lt;/p&gt;
&lt;p&gt;&lt;code&gt;stern bepoz -n bepoz-namespace --exclude-container istio&lt;/code&gt; 왼쪽과 같이 입력하면 bepoz-namespace 네임스페이스에서 bepoz 이름이 들어간 파드들의 로그를 확인하는데 istio 이름이 들어간 컨테이너는 배제시키는 명령어다.&lt;/p&gt;
&lt;p&gt;여러 옵션과 사용법이 있기 때문에 자세한 사용법은 &lt;a href=&quot;https://github.com/stern/stern&quot;&gt;https://github.com/stern/stern&lt;/a&gt; 공식 README를 참조하자.  &lt;/p&gt;
&lt;hr&gt;
&lt;/div&gt;</description>
      <category>Infra</category>
      <author>Bepoz</author>
      <guid isPermaLink="true">https://bepoz-study-diary.tistory.com/433</guid>
      <comments>https://bepoz-study-diary.tistory.com/433#entry433comment</comments>
      <pubDate>Mon, 24 Jul 2023 14:28:29 +0900</pubDate>
    </item>
  </channel>
</rss>