<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코딩공장공장장</title>
    <link>https://developer111.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 6 Jul 2026 04:22:48 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>코딩공장공장장</managingEditor>
    <item>
      <title>리액티브(반응형) 프로그래밍</title>
      <link>https://developer111.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8B%B0%EB%B8%8C%EB%B0%98%EC%9D%91%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;리액티브(반응형) 프로그래밍이란&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 흔히 사용하는 리액티브의 반대되는 방식이 명령형 프로그래밍 방식이다.&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;리액티브 프로그래밍은 데이터 흐름과 변화에 따라 반응을 정의하는 방식이다.&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;내용이 추상적이기에 이해하기 쉽게 간단한 예제를 들겠다.&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;&lt;b&gt;[명령형 프로그래밍]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qgDFc/dJMcahdwoAk/6znbtjn8P61Zp4y8FcanB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qgDFc/dJMcahdwoAk/6znbtjn8P61Zp4y8FcanB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qgDFc/dJMcahdwoAk/6znbtjn8P61Zp4y8FcanB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqgDFc%2FdJMcahdwoAk%2F6znbtjn8P61Zp4y8FcanB0%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;1076&quot; height=&quot;146&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;그 다음 메서드를 순차적으로 처리하는 방식을 갖는다.(동기)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A, B, C 메서드가 동시에 실행되는 경우는 존재하지 않는다.(블로킹)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[리액티브 프로그래밍]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1053&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhs8EZ/dJMcaffJGMu/n8JDTuOxnCmk24bamZFAkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhs8EZ/dJMcaffJGMu/n8JDTuOxnCmk24bamZFAkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhs8EZ/dJMcaffJGMu/n8JDTuOxnCmk24bamZFAkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhs8EZ%2FdJMcaffJGMu%2Fn8JDTuOxnCmk24bamZFAkk%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;1053&quot; height=&quot;142&quot; data-origin-width=&quot;1053&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;리액티브 프로그래밍에서는 위와 같이 전달해야할 input 데이터가 모두 도착하지 않더라도 1건씩 수행시켜줄 수 있다.&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;343&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba4Gvf/dJMcagTdzdq/mgevews2ZLGjMmJk4TcHp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba4Gvf/dJMcagTdzdq/mgevews2ZLGjMmJk4TcHp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba4Gvf/dJMcagTdzdq/mgevews2ZLGjMmJk4TcHp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba4Gvf%2FdJMcagTdzdq%2Fmgevews2ZLGjMmJk4TcHp0%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;1054&quot; height=&quot;343&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;343&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;위와 같은 방식을 DB에서 데이터를 가져와 소켓에서 IO 작업을 통해 프로그래밍 단에 가져와서 처리하는 방식에 대입해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 용량이 큰 100건의 데이터를 가져왔다고 해보자.&lt;br /&gt;컴퓨터의 IO 작업 또한 100건을 한번에 처리하는 것이 아니라 버퍼 크기에 따라 나누어서 읽어오게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100건이 다 될때까지 기다리는게 아니라, 되는대로 가져와서 처리하기 때문에 대기 시간이 짧아지게 된다.&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;명령형 프로그래밍의 경우 IO 작업을 하는 동안 블로킹 처리되기 때문에 CPU 연산을 동시에 수행할 수 없다.&lt;br /&gt;반면 리액티브 프로그래밍의 경우 IO 작업을 하는 동안 블로킹 되지 않고 메서드를 계속해서 실행할 수 있어&amp;nbsp;&lt;br /&gt;적은 자원으로 더많은 처리를 수행할 수 있다.&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;리액티브 프로그래밍을 구축할 때 중요한 부분으 데이터 파이프라인이 끊기지 않고 지속되도록 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령형 프로그래밍의 경우 절차만 구축해놓으면 되지만,&lt;br /&gt;리액티브 프로그래밍은 데이터의 발생을 중심으로 메서드가 수행되기에&lt;br /&gt;데이터의 흐름에 따른 프로그래밍 구축에 집중해야한다.&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Language/자바&amp;amp;코틀린</category>
      <author>코딩공장공장장</author>
      <guid isPermaLink="true">https://developer111.tistory.com/300</guid>
      <comments>https://developer111.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8B%B0%EB%B8%8C%EB%B0%98%EC%9D%91%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D#entry300comment</comments>
      <pubDate>Thu, 28 May 2026 14:37:24 +0900</pubDate>
    </item>
    <item>
      <title>spring-shardingSphere를 통한 샤딩 기초 실습</title>
      <link>https://developer111.tistory.com/entry/spring-shardingSphere%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%83%A4%EB%94%A9-%EA%B8%B0%EC%B4%88-%EC%8B%A4%EC%8A%B5</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 공유&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring boot 환경에서 sharding-sphere를 적용하는데 버전 이슈 등으로 연동이 잘 안되는 문제가 있어,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle 파일 등 전문을 공유하겠다.&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;&lt;b&gt;[build.gradle.kts]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760340539289&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    kotlin(&quot;jvm&quot;) version &quot;1.9.25&quot;
    kotlin(&quot;plugin.spring&quot;) version &quot;1.9.25&quot;
    id(&quot;org.springframework.boot&quot;) version &quot;3.5.6&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.7&quot;
    kotlin(&quot;plugin.jpa&quot;) version &quot;1.9.25&quot;
}

group = &quot;org.example&quot;
version = &quot;0.0.1-SNAPSHOT&quot;
description = &quot;spring-sharding&quot;

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-jdbc&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-jpa&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    implementation(&quot;org.apache.shardingsphere:shardingsphere-jdbc:5.5.0&quot;) {
        exclude(group = &quot;org.apache.shardingsphere&quot;, module = &quot;shardingsphere-test-util&quot;)
    }

    implementation(&quot;com.fasterxml.jackson.module:jackson-module-kotlin&quot;)
    implementation(&quot;org.jetbrains.kotlin:kotlin-reflect&quot;)
    developmentOnly(&quot;org.springframework.boot:spring-boot-devtools&quot;)
    runtimeOnly(&quot;com.mysql:mysql-connector-j&quot;)
    testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
    testImplementation(&quot;org.jetbrains.kotlin:kotlin-test-junit5&quot;)
    testRuntimeOnly(&quot;org.junit.platform:junit-platform-launcher&quot;)
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll(&quot;-Xjsr305=strict&quot;)
    }
}

allOpen {
    annotation(&quot;jakarta.persistence.Entity&quot;)
    annotation(&quot;jakarta.persistence.MappedSuperclass&quot;)
    annotation(&quot;jakarta.persistence.Embeddable&quot;)
}

tasks.withType&amp;lt;Test&amp;gt; {
    useJUnitPlatform()
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[application.yml]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760340626447&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    url: jdbc:shardingsphere:classpath:sharding.yml
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect&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;&lt;b&gt;[sharding.yml]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760340646005&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dataSources:
  ds0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3306/ds0?useSSL=false&amp;amp;allowPublicKeyRetrieval=true&amp;amp;serverTimezone=UTC&amp;amp;characterEncoding=UTF-8
    username: root
    password: 1111
  ds1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3306/ds1?useSSL=false&amp;amp;allowPublicKeyRetrieval=true&amp;amp;serverTimezone=UTC&amp;amp;characterEncoding=UTF-8
    username: root
    password: 1111

rules:
  - !SHARDING
    tables: # 샤딩할 테이블
      t_order:
        actualDataNodes: ds${0..1}.t_order
        keyGenerateStrategy:
          column: order_id
          keyGeneratorName: snowflake

    defaultDatabaseStrategy: # 데이터베이스 샤딩 전략
      standard:
        shardingColumn: order_id
        shardingAlgorithmName: database_inline

    defaultTableStrategy: # 테이블 샤딩 전략
      none:

    shardingAlgorithms: # 샤딩 알고리즘
      database_inline:
        type: INLINE
        props:
          algorithm-expression: ds${order_id % 2}

    keyGenerators: # 키 생성기
      snowflake:
        type: SNOWFLAKE
        props:
          worker-id: 123

props:
  sql-show: true&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[TOrderEntity class]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760340660749&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Table(name = &quot;t_order&quot;)
@Entity
class TOrderEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name= &quot;order_id&quot;)
    var id: Long? = null

    @Column(name = &quot;product_name&quot;)
    var productName: String = &quot;&quot;
}&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;&lt;b&gt;ddl&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760341630240&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create database ds0;
create database ds1;

-- ds0와 ds1에 각각 생성
CREATE TABLE `t_order` (
  `order_id` bigint NOT NULL,
  `product_name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`order_id`)
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;라우팅 실습&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러&lt;/p&gt;
&lt;pre id=&quot;code_1760340808819&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/order&quot;)
@RestController
class TOrderController(
    private val orderService: TOrderService
) {
    @PostMapping
    fun create(): String {
        val id = orderService.create()
        return &quot;id: ${id}&quot;
    }
}&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;서비스&lt;/p&gt;
&lt;pre id=&quot;code_1760340818231&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class TOrderService(
    private val orderRepository: TOrderRepository
) {
    fun create(): Long {
        val saveEntity = TOrderEntity().apply {
            this.productName = &quot;123&quot;
        }
        orderRepository.save(saveEntity)
        return saveEntity.id!!
    }

}&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;리포지토리&lt;/p&gt;
&lt;pre id=&quot;code_1760340829530&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface TOrderRepository : JpaRepository&amp;lt;TOrderEntity, Long&amp;gt;&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;위와 같은 layerd 계층에 따라 구현한 이후 테스트를 수행하였다.&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;포스트만을 통해 몇번의 생성 요청을 날렸더니 결과가 아래와 같이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지가 0인 데이터와 1인 데이터가 명확하게 ds0, ds1로 나뉘어 저장되었다.&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;&lt;b&gt;ds0&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;185&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nFvTl/btsQ6H3PGFa/ChWUkQG0O7wRc2XTmxSil0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nFvTl/btsQ6H3PGFa/ChWUkQG0O7wRc2XTmxSil0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nFvTl/btsQ6H3PGFa/ChWUkQG0O7wRc2XTmxSil0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnFvTl%2FbtsQ6H3PGFa%2FChWUkQG0O7wRc2XTmxSil0%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;267&quot; height=&quot;185&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;185&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&lt;b&gt;ds1&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;247&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPZdEl/btsQ6ayntFQ/RgKGMQjKaUn7IMrAdXFH6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPZdEl/btsQ6ayntFQ/RgKGMQjKaUn7IMrAdXFH6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPZdEl/btsQ6ayntFQ/RgKGMQjKaUn7IMrAdXFH6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPZdEl%2FbtsQ6ayntFQ%2FRgKGMQjKaUn7IMrAdXFH6k%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;247&quot; height=&quot;148&quot; data-origin-width=&quot;247&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분산 트랜잭션 실습&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스&lt;/p&gt;
&lt;pre id=&quot;code_1760342153139&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
@Service
class TOrderService(
    private val orderRepository: TOrderRepository,
    private val em: EntityManager
) {
    fun update() {
        val updateEntity1 = orderRepository.findById(1184508071930998784).get()
        updateEntity1.apply {
            this.productName = &quot;수정&quot;
        }
        orderRepository.save(updateEntity1)
        em.flush()

        // 제약조건 위반
        val updateEntity2 = orderRepository.findById(1184534486835507201).get()
        updateEntity2.apply {
            this.productName = &quot;수정 제약 조건 10글자 넘어감&quot;
        }
        orderRepository.save(updateEntity2)
    }

}&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;첫번째 데이터는 ds0에 존재하고, 두번째 데이터는 ds1에 존재하는 상황에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째는 성공, 두번째는 실패하도록 코드를 구현하였다.&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;실행 결과&lt;/p&gt;
&lt;pre id=&quot;code_1760342241443&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-10-13T16:53:48.546+09:00  INFO 11784 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Logic SQL: select te1_0.order_id,te1_0.product_name from t_order te1_0 where te1_0.order_id=?
2025-10-13T16:53:48.546+09:00  INFO 11784 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Actual SQL: ds0 ::: select te1_0.order_id,te1_0.product_name from t_order te1_0 where te1_0.order_id=? ::: [1184508071930998784]
2025-10-13T16:53:48.633+09:00  INFO 11784 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Logic SQL: update t_order set product_name=? where order_id=?
2025-10-13T16:53:48.634+09:00  INFO 11784 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Actual SQL: ds0 ::: update t_order set product_name=? where order_id=? ::: [수정, 1184508071930998784]
2025-10-13T16:53:48.655+09:00  INFO 11784 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Logic SQL: select te1_0.order_id,te1_0.product_name from t_order te1_0 where te1_0.order_id=?
2025-10-13T16:53:48.655+09:00  INFO 11784 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Actual SQL: ds1 ::: select te1_0.order_id,te1_0.product_name from t_order te1_0 where te1_0.order_id=? ::: [1184534486835507201]
2025-10-13T16:53:48.659+09:00  INFO 11784 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Logic SQL: update t_order set product_name=? where order_id=?
2025-10-13T16:53:48.659+09:00  INFO 11784 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Actual SQL: ds1 ::: update t_order set product_name=? where order_id=? ::: [수정 제약 조건 10글자 넘어감, 1184534486835507201]
2025-10-13T16:53:48.683+09:00  WARN 11784 --- [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1406, SQLState: 22001
2025-10-13T16:53:48.683+09:00 ERROR 11784 --- [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : Data truncation: Data too long for column 'product_name' at row 1
2025-10-13T16:53:48.712+09:00 ERROR 11784 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.DataIntegrityViolationException: could not execute statement [Data truncation: Data too long for column 'product_name' at row 1] [update t_order set product_name=? where order_id=?]; SQL [update t_order set product_name=? where order_id=?]] with root cause

cohttp://m.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'product_name' at row 1&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;커밋에 실패하였고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 db에 접근하여 데이터를 확인해보니 첫번째 데이터가 변경되지 않았으므로 데이터의 일관성이 지켜진것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 트랜잭션이 적용되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;샤딩 키 조회&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;샤딩 키 존재시&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760343273455&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun get(id: Long): String {
    val entity = orderRepository.findById(id).get()
    return &quot;id : ${entity.id}, productName : ${entity.productName}&quot;
}&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;샤딩키를 통해 라우팅할 노드가 정해지므로&amp;nbsp; Actual SQL 또한 해당 샤드 한곳에서 한번 실행된다.&lt;/p&gt;
&lt;pre id=&quot;code_1760343296037&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-10-13T17:14:46.594+09:00  INFO 28584 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Logic SQL: select te1_0.order_id,te1_0.product_name from t_order te1_0 where te1_0.order_id=?
2025-10-13T17:14:46.594+09:00  INFO 28584 --- [nio-8080-exec-1] ShardingSphere-SQL                       : Actual SQL: ds1 ::: select te1_0.order_id,te1_0.product_name from t_order te1_0 where te1_0.order_id=? ::: [1184534486835507201]&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;&lt;b&gt;샤딩 키 미존재시&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760343260569&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun getByProductName(productName: String): List&amp;lt;Long&amp;gt; {
    val entityList = orderRepository.findByProductName(productName)
    return entityList.map{ it.id as Long }
}&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;아래와 같이 Logical SQL은 하나이지만 Acutal SQL은 각 노드마다 발생하므로 두번의 sql이 실행된다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760343196020&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-10-13T17:12:33.647+09:00  INFO 28584 --- [nio-8080-exec-6] ShardingSphere-SQL                       : Logic SQL: select te1_0.order_id,te1_0.product_name from t_order te1_0 where te1_0.product_name=?
2025-10-13T17:12:33.647+09:00  INFO 28584 --- [nio-8080-exec-6] ShardingSphere-SQL                       : Actual SQL: ds0 ::: select te1_0.order_id,te1_0.product_name from t_order te1_0 where te1_0.product_name=? ::: [123]
2025-10-13T17:12:33.647+09:00  INFO 28584 --- [nio-8080-exec-6] ShardingSphere-SQL                       : Actual SQL: ds1 ::: select te1_0.order_id,te1_0.product_name from t_order te1_0 where te1_0.product_name=? ::: [123]&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;샤딩 미적용 테이블&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;pre id=&quot;code_1760344280535&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rules: 
  - !SHARDING
  ...
  
  # 단일 테이블 규칙
  - !SINGLE
    tables:
      - ds0.member&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤딩 미적용 테이블은 위와 같이 싱글 DB 경로를 적어주면 된다.&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Framework &amp;amp; Lib/스프링</category>
      <author>코딩공장공장장</author>
      <guid isPermaLink="true">https://developer111.tistory.com/296</guid>
      <comments>https://developer111.tistory.com/entry/spring-shardingSphere%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%83%A4%EB%94%A9-%EA%B8%B0%EC%B4%88-%EC%8B%A4%EC%8A%B5#entry296comment</comments>
      <pubDate>Mon, 13 Oct 2025 20:35:59 +0900</pubDate>
    </item>
    <item>
      <title>[RDB 부하 분산3] - 샤딩</title>
      <link>https://developer111.tistory.com/entry/RDB-%EB%B6%80%ED%95%98-%EB%B6%84%EC%82%B03-%EC%83%A4%EB%94%A9</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;샤딩이란&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 여러 서버에 분산 저장하는 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 여러개 두어 scale-out하는 기법으로&amp;nbsp;단일 DB의 서버의 용량과 성능의 한계를 극복할 수 있는 기법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;샤딩의 장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성능 향상&lt;br /&gt;여러 DB 인스턴스에 데이터를 나누고 각 데이터가 존재하는 DB에서 연산을 수행하므로&lt;br /&gt;부하와 데이터가 분산되어 쿼리 성능 향상을 이룰 수 있다.&lt;/li&gt;
&lt;li&gt;장애 격리&lt;/li&gt;
&lt;li&gt;샤드 단위 백업 및 복구&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;샤딩의 단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일괄성 유지 어려움&lt;b&gt;&lt;br /&gt;&lt;/b&gt;- 분산 트랜잭션 처리 및 데이터 동기화에 대한 관리 필요&lt;br /&gt;- auto_increment 충돌 (충돌 방지 미들웨어 사용해야함, sharding-sphere)&lt;/li&gt;
&lt;li&gt;복잡성 증가&lt;br /&gt;조인 연산이나 복잡한 쿼리는 샤딩 이전 환경의 쿼리보다 느리고 에러 발생 빈도가 높아질 수 있다.&lt;/li&gt;
&lt;li&gt;샤드 불균형 발생 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤딩은 스케일업으로 DB 스펙이 한계에 다다랐을 때, scale-out을 통해 부하를 분산시킬 수 있는 기법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량/고부하 환경에서 큰 성능 향상을 가져다 줄 수 있지만 데이터 분산으로 인한 데이터의 일관성 문제, 관리의 복잡성을 고려해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;샤딩 알고리즘&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;모듈러 샤딩&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pk를 모듈러 연산하여 저장할 샤드를 결정하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Range 샤딩에 비해 데이터가 균일하게 분산되어 부하를 균일하게 분산시킬 수 있다는 장점이 있으나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 추가 증설 과정에서 이미 적재된 데이터의 재정렬이 필요하다는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단 나누는 수인 제수의 배수로 하게 되면 재정렬을 피할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 서버를 3대로 구성하면 3으로 나누어 나머지가 0은 db0, 1은 db1, 2은 db2에서&lt;br /&gt;서버를 6대로 증설하면 0, 1, 2를 기존 DB 연결을 유지한채 새로운 데이터의 접근 경로를 바꿀 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(데이터가 지속적으로 증가하는 서비스보다 일정 수준에 도달한 이후 증가 속도가 빠르지 않은 서비스에 적합하다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Range 샤딩&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pk 값의 범위를 기준으로 샤드를 결정하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈러 샤딩에 비해 DB 추가 증설 상황에서 리밸런싱이 발생하지 않는다는 장점이 있어, 유지보수에 용이한 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 특정 DB로 부하가 집중될 수 있다는 단점이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CDC(Change Data Capture) - 실시간 데이터 동기화&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조를 샤딩 구조로 변경하는 과정에서 원본 DB에 있는 데이터를 새로운 샤드로 복제하는 과정이 필요할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 사용되는 개념이 실시간 데이터 동기화를 위한 CDC이다.&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;CDC란, 데이터베이스의 변경사항을 실시간으로 감지하고 이를 다른 시스템에 전달하는 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CDC 구현 방식&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Trigger&lt;/b&gt; : DB에서 트리거가 발생하여 기록하는 방식. &lt;br /&gt;구현이 간단하지만 DB 성능에 영향을 줄 수 있어 대용량 환경에는 적합하지 않다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Log&lt;/b&gt; : DB의 트랜잭션 로그를 읽어 변경사항을 식별.&amp;nbsp;&lt;br /&gt;성능에 미치는 영향이 가장 적어, 대용량 환경에서 적합&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Polling&lt;/b&gt; : 주기적으로 변경사항을 찾아내는 방식.&lt;br /&gt;가장 간단한 구현 모델이지만, 실시간성이 떨어질 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CDC 사례&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.gmarket.com/61&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.gmarket.com/61&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 지마켓 샤딩 사례에서 실시간 데이터 복제 관련한 내용이 서술되어있다.&lt;br /&gt;Debezium과 같은 오픈 소스도 존재하지만 가용 리소스 문제로 직접 서비스를 구축하여 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경사항 키값을 기록하고 이를 통해 다시한번 변경 내용을 가져와 타겟 DB에 복제하는 방식인데 위 글을 읽기 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;샤딩 스피어&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤딩 스피어는 분산 DB 환경을 구축하는 미들웨어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 DB 환경에서 필요한 라우팅과 데이터 일관성을 위한 분산 트랜잭션과 같은 기능을 제공한다.&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;&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&gt;라우팅&lt;/li&gt;
&lt;li&gt;결과 병합&lt;/li&gt;
&lt;li&gt;분산 트랜잭션 지원 (XA와 Base 트랜잭션 혼합)&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;ol style=&quot;list-style-type: decimal; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SQL Parse&lt;br /&gt;SQL문을 형태소 단위로 분해&lt;/li&gt;
&lt;li&gt;Query Optimize&lt;br /&gt;분해한 SQL문을 최적화&lt;/li&gt;
&lt;li&gt;SQL Route&lt;br /&gt;샤딩 룰, 샤딩 키를 분석해 대상 DB, TABLE 선정&lt;/li&gt;
&lt;li&gt;SQL Rewrite&lt;br /&gt;선정 된 DB, TABLE에 기반하여 SQL문 재작성&lt;/li&gt;
&lt;li&gt;SQL Execute&lt;br /&gt;재작성된 SQL문 실행&lt;/li&gt;
&lt;li&gt;Result Merge&lt;br /&gt;각 SQL문 결과들을 Merge하여 하나의 테이블에 접근한 것 같은 통합된 ResultSet을 제공&lt;/li&gt;
&lt;/ol&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[샤딩스피어의 종류]&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 227px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.9147%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 38.2171%; text-align: center; height: 17px;&quot;&gt;&lt;b&gt;ShardingSphere-JDBC&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; text-align: center; height: 17px;&quot;&gt;&lt;b&gt;ShardingSphere-Proxy&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 18.9147%; height: 21px;&quot;&gt;동작 위치&lt;/td&gt;
&lt;td style=&quot;width: 38.2171%; height: 21px;&quot;&gt;자바 애플리케이션의 jdbc 계층&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 21px;&quot;&gt;별도의 독립된 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 18.9147%; height: 21px;&quot;&gt;클라이언트 지원&lt;/td&gt;
&lt;td style=&quot;width: 38.2171%; height: 21px;&quot;&gt;자바 애플리케이션&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 21px;&quot;&gt;모든 언어 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 18.9147%; height: 21px;&quot;&gt;복잡도&lt;/td&gt;
&lt;td style=&quot;width: 38.2171%; height: 21px;&quot;&gt;라이브러리와 yml을 통한 간단한 설정&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 21px;&quot;&gt;별도 서버 구성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 18.9147%; height: 21px;&quot;&gt;성능&lt;/td&gt;
&lt;td style=&quot;width: 38.2171%; height: 21px;&quot;&gt;네트워크 오버헤드 없음&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 21px;&quot;&gt;네트워크 오버헤드 존재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 18.9147%; height: 21px;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 38.2171%; height: 21px;&quot;&gt;별도의 서버 자원이 필요없고 설정이 간단함.&lt;br /&gt;jvm 내부에서 동작하므로 성능이 우수하고 네트워크 오버헤드가 미존재&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 21px;&quot;&gt;여러 애플리케이션에서 공유하여 사용하기 적함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 18.9147%; height: 21px;&quot;&gt;단점&lt;/td&gt;
&lt;td style=&quot;width: 38.2171%; height: 21px;&quot;&gt;자바 이외 애플리케이션 사용불가&lt;br /&gt;애플리케이션이 늘어나면 별도의 설정 필요&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 21px;&quot;&gt;네트워크 오버헤드가 존재하고,&lt;br /&gt;프록시 서버 장애시 전체 장애로 전파됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 63px;&quot;&gt;
&lt;td style=&quot;width: 18.9147%; height: 63px;&quot;&gt;사용사례&lt;/td&gt;
&lt;td style=&quot;width: 38.2171%; height: 63px;&quot;&gt;하나의 애플리케이션에서만 샤드에 접근하는 경우&lt;br /&gt;공통된 설정으로 모두 적용가능하여 사용하기 적합&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 63px;&quot;&gt;여러 언어를 사용하거나,&lt;br /&gt;서비스가 다른 여러 자바 애플리케이션을 운영할때,&lt;br /&gt;프록시 서버를 구축하여 통합 관리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;XA&lt;/b&gt;&lt;br /&gt;2pc 모델을 통해 분산 트랜잭션을 관리함&lt;br /&gt;강한 일관성을 얻을 수 있으나, 확장성과 처리량이 떨어짐&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Base 트랜잭션&lt;/b&gt;&lt;br /&gt;Eventually Consistency를 모델로 결과적 일관성을 확보하는 것을 목표로함&lt;br /&gt;확장성과 처리량이 뛰어나지만, 일관성이 느슨함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고.&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://shardingsphere.apache.org/document/current/en/overview/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;아피치 샤딩스피어 오피셜&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤딩은 DB 부하 분산에 큰 역할을 수행할 수 있으나, 데이터 일관성이 깨지거나, 부하 불균형이 발생할 수 있다.&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;/p&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;p data-ke-size=&quot;size16&quot;&gt;&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;&lt;a href=&quot;https://dev.gmarket.com/61&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;지마켓 - Shared Mysql Cluster&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.yogiyo.co.kr/%EC%A3%BC%EB%AC%B8%EC%84%9C%EB%B9%84%EC%8A%A4-shardingsphere-proxy-%EB%8F%84%EC%9E%85%EA%B8%B0-46d83084586b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;요기요 -&amp;nbsp; 주문서비스 ShardingSphere-Proxy&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://shardingsphere.apache.org/document/current/en/overview/&quot;&gt;아피치 샤딩스피어 오피셜&lt;/a&gt; &amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://shardingsphere.apache.org/document/5.0.0/en/user-manual/shardingsphere-jdbc/usage/sharding/java-api/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;샤딩 실습&lt;/a&gt;&lt;/p&gt;</description>
      <category>DB/mysql</category>
      <author>코딩공장공장장</author>
      <guid isPermaLink="true">https://developer111.tistory.com/294</guid>
      <comments>https://developer111.tistory.com/entry/RDB-%EB%B6%80%ED%95%98-%EB%B6%84%EC%82%B03-%EC%83%A4%EB%94%A9#entry294comment</comments>
      <pubDate>Wed, 8 Oct 2025 13:04:42 +0900</pubDate>
    </item>
    <item>
      <title>[RDB 부하 분산2] - 파티셔닝</title>
      <link>https://developer111.tistory.com/entry/RDB-%EB%B6%80%ED%95%98-%EB%B6%84%EC%82%B02-%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9D</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;파티셔닝(Partitioning)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&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;p data-ke-size=&quot;size16&quot;&gt;mysql 파티션은 파티션 단위로 테이블과 인덱스가 모두 분리되기에 파티션 키 설정이 매우 중요하다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파티션이 필요한 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 테이블에 너무 많은 레코드가 쌓이는 경우,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스캔 범위가 넓어지는 것 뿐만 아니라 레코드나 인덱스를 메모리에 올리지 못하여 디스크 접근을 유발하여 더더욱 성능 저하를 유발할 수 있다&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이때, 하나의 테이블을 물리적으로 여러 파티션으로 나누게 되면 파티션 단위 접근과 파티션 단위 로컬 인덱스를 사용 가능하므로&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;스캔 범위를 줄이는 것 뿐만 아니라 로컬 파티션 인덱스마저 사용 가능하니 성능 향상을 더욱 기대할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;파티션의 장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 성능 향상
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조건에 맞는 파티션만 스캔하여 불필요한 I/O를 감소하여 쿼리 성능 향상&lt;br /&gt;(레코드를 읽을때 모든 칼럼을 읽어내는데 범위를 줄이므로 성능 향상됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;쓰기 성능 향상&lt;br /&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파티션 단위 백업 및 복구 시간 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;파티션의 단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블간의 조인 비용 증가&lt;br /&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파티션 키로 조회하지 않을시 전체 파티션 스캔(full partition scan)&lt;/li&gt;
&lt;li&gt;테이블과 인덱스를 별도로 파티셔닝할 수 없다.(mysql만, 오라클은 인덱스 별도 가능)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블만, 인덱스만 별도로 파티셔닝 불가하고 반드시 함께 파티셔닝된다.&lt;br /&gt;즉, 파티션 단위 내에서만 인덱스가 적용됨&lt;/li&gt;
&lt;li&gt;특정 조건의 인덱스만 별도로 파티션 불가하며, 물리적인 파티션 단위로 함께 나눠야함&lt;br /&gt;즉, 다양한 조건에 따른 인덱스 파티션이 불가하고 물리적인 단위로 나눠야함&lt;br /&gt;파티션 내에서 인덱스를 별도로 관리해야함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;Mysql 파티셔닝 종류&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;1. 수직(Vertical) 파티셔닝&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;칼럼의 크기가 큰 경우 key 기반으로 분리시키게되면 레코드 조회시 성능 향상을 기대할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 컨텐츠의 목록을 조회하는 경우 본문은 제외하고 메타 정보만 조회해와야하는 경우가 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 크기가 큰 본문 내용이 분리되어있는 경우, 레코드 조회시 IO 작업을 줄여줄 수 있으므로 쿼리 성능 향상을 기대할 수 있다.&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;/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;(수직 파티셔닝은 별도의 파티션 구문이 지원되는 것으로 아니고 테이블 최적화에 가까워,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 일반적을 이야기하는 파티셔닝은 수직 파티셔닝이 아닌 수평 파티셔닝이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;2. 수평(horizontal) 파티셔닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행 기준으로 파티션을 나눈 것&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;&lt;b&gt;Range 파티셔닝(범위 분할)&lt;/b&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;이력 테이블에서 연도별 파티션을 나누는데 사용할 수 있다.&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;&lt;b&gt;List 파티셔닝(목록 분할)&lt;/b&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;국가, 지역, 카테고리 등으로 분할에 많이 사용된다.&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;&lt;b&gt;Hash 파티셔닝&lt;/b&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;데이터의 균등한 분포가 중요한 경우 사용될 수 있다.&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;&lt;b&gt;Key 파티셔닝&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 파티셔닝의 일종으로 PK 기반으로 분할한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파티션과 인덱스&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysql은 로컬 파티션 인덱스만 지원, 오라클은 전부 지원&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;pk, unique key에 파티션 키를 포함해야함(세컨더리 인덱스는 상관 없음)&lt;/li&gt;
&lt;li&gt;전체 테이블 조회시, 각 파티션의 인덱스 검색 이후 병합 과정으로 성능 저하 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/ul&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;style3&quot;&gt;&lt;b&gt;글로벌 파티션 인덱스를 거의 사용하지 않는 이유?&lt;/b&gt;&lt;br /&gt;사용성을 보면 로컬 파티션 인덱스가 압도적으로 높다.&lt;br /&gt;로컬 파티션 인덱스는 각 파티션 내에서 독립적으로 관리되기에 유지보수가 쉽다.&lt;br /&gt;글로벌 파티션 인덱스는 파티션 drop 시 인덱스가 깨질 수 있다.&lt;br /&gt;반면 로컬 파티션 인덱스는 독립적으로 관리되기에 그러한 상황이 발생하지 않는다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파티션 인덱스 사용시 주의사항&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysql 파티션 인덱스는 로컬 파티션 인덱스를 지원하기에 기존에 인덱스를 잘 활용하기 위해서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 키가 검색 조건에 자주 사용되는 것으로 설정해야 파티션과 인덱스 모두 잘 활용할 수 있다.&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;&lt;b&gt;i) 파티션 포함, 인덱스 포함&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;해당 파티션만 접근하고 해당 파티션 내의 인덱스를 사용하므로 가장 최상의 조건인 쿼리이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ii) 파티션 미포함, 인덱스 포함&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;full partition scan이 발생하고 각 파티션 내의 인덱스를 통해 스캔하고 결과를 병합한다.&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;/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;&lt;b&gt;ii) 파티션 포함, 인덱스 미포함&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 파티션 내에서만 full scan을 하여 전체 full scan 보다 성능은 향상될 수 있지만, 인덱스를 활용하지 못하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;파티션 제약 조건&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pk와 unique key들은 모두 파티션 키 칼럼을 포함해야한다.&lt;/li&gt;
&lt;li&gt;외래키 사용 불가&lt;br /&gt;외래키는 부모-자식 간의 참조가 발생하고 삽입, 삭제, 수정 간에 참조여부를 확인하는데&lt;br /&gt;Mysql이 어느 파티션에서 참조되는지 확인하기 어렵기 때문에 파티션 테이블에 FK를 지원하지 않는다.&lt;/li&gt;
&lt;li&gt;temporary table은 파티션 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 키가 pk나 unique key에 포함되어야하는 것, 외래키가 사용 불가능한 제약조건들은 개발 난이도를 상승 시키고&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 잘못된 설계는 성능을 오히려 저하시킬 수 있기에 파티셔닝 적용은 복제와 같은 수단 이후에 적용하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;파티션 적용이 어려운 이유&lt;/b&gt;&lt;br /&gt;첫째, 개발 측면에서 파티션 키는 PK나 UNIQUE KEY에 포함되어야 하고, 외래 키 사용이 제한되기 때문에 ORM 설계나 쿼리 작성이 복잡해진다.&lt;br /&gt;둘째, 설계 단계에서는 어떤 컬럼을 파티션 키로 선택할지, 어떤 파티셔닝 알고리즘을 쓸지 결정해야 하는데, 잘못 선택하면 특정 파티션에 데이터가 집중되는 &amp;lsquo;Hot Partition&amp;rsquo; 문제가 발생해 성능이 오히려 떨어질 수 있다.&lt;br /&gt;셋째, 운영과 관리 측면에서는 파티션 단위 백업, 모니터링, DDL 작업 등이 복잡하기 때문에 관리 부담이 증가한다.&lt;br /&gt;이런 이유로, 보통은 인덱스 최적화나 복제 같은 기본 성능 개선 수단을 먼저 적용하고, 그래도 부하가 발생한다면 파티셔닝을 적용한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파티션 실습&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 지역코드를 파티션 키로 해시 파티셔닝을 수행해보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1759911883711&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE `contents_range`(
    `id` int NOT NULL,
    `region_code` int NOT NULL,
    `title` varchar(100) NOT NULL,
    `author_id` int not null,
    `contents` TEXT NOT NULL,
    `create_at` DATETIME NOT NULL,
    `update_at` DATETIME NOT NULL,
    `delete_at` DATETIME
)
PARTITION BY KEY (region_code)
PARTITIONS 5;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;외래키 생성불가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 구문은 파티션 테이블을 자식테이블로 외래키를 설정한 구문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1759912065638&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE IF NOT EXISTS `contents_parent` (
    `id` INT NOT NULL AUTO_INCREMENT,
    PRIMARY KEY (id)
);

CREATE TABLE `contents_partition`(
    `id` int NOT NULL,
    `parent_id` int not null,
    `region_code` int NOT NULL,
    `title` varchar(100) NOT NULL,
    `author_id` int not null,
    `contents` TEXT NOT NULL,
    `create_at` DATETIME NOT NULL,
    `update_at` DATETIME NOT NULL,
    `delete_at` DATETIME,
    FOREIGN KEY(parent_id) REFERENCES contents_parent(id)
)
PARTITION BY KEY (region_code)
PARTITIONS 5;&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;Error Code 1506으로 파티션 테이블에 외래키 생성 불가하다는 오류를 발생시킬 것이다.&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;이번에는 파티션 테이블을 부모테이블로 참조하는 자식 테이블을 생성해보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1759912232563&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE `contents_partition`(
    `id` int NOT NULL,
    `region_code` int NOT NULL,
    `title` varchar(100) NOT NULL,
    `author_id` int not null,
    `contents` TEXT NOT NULL,
    `create_at` DATETIME NOT NULL,
    `update_at` DATETIME NOT NULL,
    `delete_at` DATETIME
)
PARTITION BY KEY (region_code)
PARTITIONS 5;

CREATE TABLE IF NOT EXISTS `contents_child` (
    `child_id` INT NOT NULL AUTO_INCREMENT,
    `parent_id` INT NOT NULL,       -- 부모 테이블 id 참조
    PRIMARY KEY (child_id),
    FOREIGN KEY(parent_id) REFERENCES contents_partition(id)
);&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;마찬가지로 Error Code 1506으로 테이블 생성이 불가하다.&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;파티션 테이블은 부모-자식 모두 생성 불가하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파티션 키 사용 - 조회 성능&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;i) 파티션 미존재 테이블 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1759912851132&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;explain
SELECT * FROM contents_org WHERE region_code = 3;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZrvXm/btsQ3YX0IJR/80lVywDvefQ8esaIm2l0m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZrvXm/btsQ3YX0IJR/80lVywDvefQ8esaIm2l0m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZrvXm/btsQ3YX0IJR/80lVywDvefQ8esaIm2l0m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZrvXm%2FbtsQ3YX0IJR%2F80lVywDvefQ8esaIm2l0m1%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;732&quot; height=&quot;56&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;56&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ii) 파티션 존재 테이블 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1759912913594&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;explain
SELECT * FROM contents_partition WHERE region_code = 3;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;53&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NxKb2/btsQ3354v7g/YlvOfLOIepupIKjJmkhzXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NxKb2/btsQ3354v7g/YlvOfLOIepupIKjJmkhzXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NxKb2/btsQ3354v7g/YlvOfLOIepupIKjJmkhzXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNxKb2%2FbtsQ3354v7g%2FYlvOfLOIepupIKjJmkhzXK%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;748&quot; height=&quot;53&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;53&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 테이블의 실행계획을 보면 비교해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 테이블은 partition 항목에&amp;nbsp; p1이라는 값이 존재한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 p1 파티션에만 접근하여 연산을 수행한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 rows를 보면 49만건과 19만건으로 2배 이상 스캔하는 레코드의 갯수가 차이가 나는 것을 알 수 있다.&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;실제 쿼리 실행시 80% 가까이 성능 차이가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;파티션 키 미사용 - 조회 성능&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;i) 파티션 미존재 테이블&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1759913880087&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;explain
SELECT * FROM contents_org;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJFVOO/btsQ2wnRC7e/dOoY5tBUnVtOgMvZmFBsM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJFVOO/btsQ2wnRC7e/dOoY5tBUnVtOgMvZmFBsM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJFVOO/btsQ2wnRC7e/dOoY5tBUnVtOgMvZmFBsM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJFVOO%2FbtsQ2wnRC7e%2FdOoY5tBUnVtOgMvZmFBsM1%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;768&quot; height=&quot;54&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;54&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ii) 파티션 존재 테이블&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1759913908547&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;explain
SELECT * FROM contents_partition;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;61&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkNZDK/btsQ32sBJwM/h3YF7KaxovcdAfcV8pi4Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkNZDK/btsQ32sBJwM/h3YF7KaxovcdAfcV8pi4Mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkNZDK/btsQ32sBJwM/h3YF7KaxovcdAfcV8pi4Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkNZDK%2FbtsQ32sBJwM%2Fh3YF7KaxovcdAfcV8pi4Mk%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;826&quot; height=&quot;61&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;61&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;파티션 존재 테이블에서 파티션 키를 사용하지 않는 경우,&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인덱스 조건 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 인덱스를 생성해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1759914455084&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE contents_partition
ADD INDEX idx_author_id (author_id);&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;&lt;b&gt;i) 파티션 키와 인덱스 모두 사용&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1759914515262&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;explain
SELECT
	* 
FROM contents_partition
where 
	author_id = 1
	and region_code=2;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;48&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhJUIQ/btsQ4mK61Za/3FAxsyy2uyhtHJCvfsnyEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhJUIQ/btsQ4mK61Za/3FAxsyy2uyhtHJCvfsnyEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhJUIQ/btsQ4mK61Za/3FAxsyy2uyhtHJCvfsnyEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhJUIQ%2FbtsQ4mK61Za%2F3FAxsyy2uyhtHJCvfsnyEk%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;854&quot; height=&quot;48&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;48&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&lt;b&gt;ii) 인덱스만 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1759914532951&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;explain
SELECT
   * 
FROM contents_partition
where 
   author_id = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;51&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zEy2U/btsQ4CtClXr/OuHIinfTDK9rk8RW4xksu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zEy2U/btsQ4CtClXr/OuHIinfTDK9rk8RW4xksu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zEy2U/btsQ4CtClXr/OuHIinfTDK9rk8RW4xksu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzEy2U%2FbtsQ4CtClXr%2FOuHIinfTDK9rk8RW4xksu1%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;817&quot; height=&quot;51&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;51&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&amp;nbsp;&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;/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;파티션 키 없이 인덱스만 사용하는 경우,&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 쿼리 성능을 오히려 저하시킬 수 있게 된다.&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/mysql</category>
      <author>코딩공장공장장</author>
      <guid isPermaLink="true">https://developer111.tistory.com/293</guid>
      <comments>https://developer111.tistory.com/entry/RDB-%EB%B6%80%ED%95%98-%EB%B6%84%EC%82%B02-%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9D#entry293comment</comments>
      <pubDate>Wed, 8 Oct 2025 13:03:37 +0900</pubDate>
    </item>
    <item>
      <title>[RDB 부하 분산1] - 쿼리 최적화</title>
      <link>https://developer111.tistory.com/entry/RDB-%EB%B6%80%ED%95%98-%EB%B6%84%EC%82%B0-%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;RDB의 부하를 줄이는 방법&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;단일 DB 내에서 최적화&lt;/h4&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;CQRS&lt;/li&gt;
&lt;li&gt;파티션&lt;/li&gt;
&lt;li&gt;스케일업&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;분산 DB를 통한 부하 분산&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;복제&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;캐싱 DB 별도 사용(redis)&lt;/li&gt;
&lt;li&gt;DB MSA&lt;/li&gt;
&lt;li&gt;샤딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스가 지속되고 사용자와 트래픽이 증가하게 되면 DB의 부하가 증가할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 가장 고려해볼만한 옵션은 쿼리 최적화나 인덱스 추가, CQRS 패턴 같은 것들이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는 단일 DB 내에서 CPU, 메모리, IO 자원을 효율적으로 사용하기 위해 고려하는 옵션이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단일 DB는 컴퓨터의 용량과 스펙이라는 한계가 존재하기 때문에,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리 최적화, 인덱스, CQRS 패턴으로는 한계에 직면할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때, 고려해볼 수 있는 옵션이 레플리케이션(복제)이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;복제는 RDB에서 제공해주는 옵션으로 쉽게 적용 가능하고 데이터의 신뢰성 측면에서도 안정적이기 때문에&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스가 커지며 비교적 빠른 시기에 도입해볼 수 있는 옵션이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만, 복제를 수행할 때 동기 복제는 트랜잭션 처리에서 복제로 인한 부하와 latency(지연)를 초래할 수 있기에,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비동기 복제를 주로 사용한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DB 접근의 대부분의 작업이 읽기 작업이라는 점을 이용하여 쓰기 작업은 master에서 읽기 작업은 slave에서 이루어지도록 구성한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;레플리케이션의 문제는 쓰기 작업의 부하가 증가하는 경우 해결할 수 없다는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때, 고려해볼 수 있는 방식은 파티셔닝이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파티셔닝은 하나의 큰 테이블을 독립적인 파티션으로 나누어 저장하기에&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 파티션이 레코드를 나누어 관리하고, 인덱스 또한 파티션 단위의 로컬 스코프를 갖게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는 쓰기 작업에서 원본 레코드의 변경 뿐만 아니라 인덱스 트리의 변경 범위를 줄여주기에 쓰기가 많은 환경에서 특히 유리하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파티셔닝은 단일 DB 내의 작업이지만 운영과 구현에 난이도가 존재하기에 복제보다 뒤에 사용하는 것이 고려된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쓰기 DB의 스펙을 최고로 향상시키고 파티셔닝을 도입하게 되었다고 하더라도 용량이 부족해지고, 부하가 지속된다면&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;샤딩을 고려할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;샤딩은 데이터 자체를 여러 DB에 나누어 저장하는 기법이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쓰기 작업의 부하도 여러 인스턴스에 나누어 저장할 수 있는 scale-out 방식이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 여러 DB에 나누어진 데이터는 트랜잭션의 일관성과 복잡한 조인은 오히려 성능 저하를 일으킬 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;쿼리 최적화&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 불필요한 조인/칼럼 제거&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무를 수행하다보면 변경사항이 빈번하게 발생하며&amp;nbsp;불필요한 조인이나 칼럼들이 남아있는 경우가 많이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인은 레코드를 읽기 위한 디스크 I/O와, 정렬 및 반복 비교 등의 CPU 연산을 필요로 한다.&lt;br /&gt;따라서 불필요한 조인을 제거하면 스캔해야 할 레코드 수가 줄어들어 I/O 부하가 감소하고,&lt;br /&gt;동시에 정렬&amp;middot;비교 연산 횟수도 줄어 전반적인 쿼리 성능이 크게 향상된다.&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;또한, DB의 부하는 생각보다 IO로 인한 부하가 많이 발생한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 칼럼은 많은 IO를 유발하여 클라이언트와 DB 서버에 모두 부하가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA의 엔티티 조회 같은 방식은 IO 자원 측면에서는 그렇게 좋은 방식은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS 패턴을 도입하여 필요한 칼럼만 조회하는 것이 IO 자원을 효율적으로 사용하여 부하를 줄여 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 조인시 인덱스 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인 칼럼을 인덱스 칼럼으로 사용하지 않는 경우 테이블 풀스캔이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 풀스캔은 디스크 접근을 통한 I/O가 발생하여 성능 저하를 크게 일으킬 수 있다.&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;개인적으로 성능 저하가 발생한 쿼리를 분석할 때, 가장 먼저 확인 하는 부분이 조인 칼럼에 인덱스 칼럼이 사용되고 있는지 여부이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그만큼 테이블 풀스캔으로 인한 성능저하가 쿼리 성능을 저하시키는 큰 요소이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. where 조건에 인덱스 조건 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;where 조건에 인덱스 사용 또한 테이블 풀스캔을 방지하기 위함이다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 실행계획&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 260px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;text-align: center; width: 24.6513%; height: 18px;&quot;&gt;&lt;b&gt;항목&lt;br /&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 24.3022%; height: 18px;&quot;&gt;&lt;b&gt;확인 방법&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 38.8372%; height: 18px;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 12.2093%; height: 18px;&quot;&gt;&lt;b&gt;성능 영향&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 24.6513%; height: 37px;&quot;&gt;&lt;b&gt;인덱스 사용 여부&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.3022%; height: 37px;&quot;&gt;EXPLAIN의 type, key&lt;/td&gt;
&lt;td style=&quot;width: 38.8372%; height: 37px;&quot;&gt;type이 ALL이면 풀스캔 (성능 저하), key에 인덱스명 있는지 확인&lt;/td&gt;
&lt;td style=&quot;width: 12.2093%; height: 37px;&quot;&gt;⭐⭐⭐&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 24.6513%; height: 37px;&quot;&gt;&lt;b&gt;임시 테이블 사용 여부&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.3022%; height: 37px;&quot;&gt;EXPLAIN의 Extra&lt;/td&gt;
&lt;td style=&quot;width: 38.8372%; height: 37px;&quot;&gt;Using temporary 문구 존재 시 임시 테이블 사용, 디스크 기반이면 더 느림&lt;/td&gt;
&lt;td style=&quot;width: 12.2093%; height: 37px;&quot;&gt;⭐ ⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 24.6513%; height: 37px;&quot;&gt;&lt;b&gt;정렬 방식 (filesort)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.3022%; height: 37px;&quot;&gt;EXPLAIN의 Extra&lt;/td&gt;
&lt;td style=&quot;width: 38.8372%; height: 37px;&quot;&gt;Using filesort는 인덱스를 이용하지 않고 정렬함 (CPU/메모리 부담)&lt;/td&gt;
&lt;td style=&quot;width: 12.2093%; height: 37px;&quot;&gt;⭐ ⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 24.6513%; height: 37px;&quot;&gt;&lt;b&gt;조인 순서 및 전략&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.3022%; height: 37px;&quot;&gt;EXPLAIN 조인 순서 확인&lt;/td&gt;
&lt;td style=&quot;width: 38.8372%; height: 37px;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;큰 테이블이 먼저 읽히거나 인덱스 없이 조인되면 느려짐&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 12.2093%; height: 37px;&quot;&gt;⭐⭐&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 24.6513%; height: 19px;&quot;&gt;&lt;b&gt;GROUP BY / DISTINCT 사용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.3022%; height: 19px;&quot;&gt;쿼리 / EXPLAIN / Extra&lt;/td&gt;
&lt;td style=&quot;width: 38.8372%; height: 19px;&quot;&gt;자주 임시 테이블 + 정렬 발생, 인덱스 지원 여부 중요&lt;/td&gt;
&lt;td style=&quot;width: 12.2093%; height: 19px;&quot;&gt;⭐⭐&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 24.6513%; height: 37px;&quot;&gt;&lt;b&gt;서브쿼리 / 상관 서브쿼리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.3022%; height: 37px;&quot;&gt;쿼리 확인&lt;/td&gt;
&lt;td style=&quot;width: 38.8372%; height: 37px;&quot;&gt;반복 실행될 수 있어 성능 이슈, JOIN으로 대체 가능성 고려&lt;/td&gt;
&lt;td style=&quot;width: 12.2093%; height: 37px;&quot;&gt;⭐⭐&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 24.6513%; height: 19px;&quot;&gt;&lt;b&gt;함수 사용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.3022%; height: 19px;&quot;&gt;WHERE절 등 함수 사용 여부&lt;/td&gt;
&lt;td style=&quot;width: 38.8372%; height: 19px;&quot;&gt;WHERE func(col) 형태는 인덱스 못 타는 경우 많음&lt;/td&gt;
&lt;td style=&quot;width: 12.2093%; height: 19px;&quot;&gt;⭐&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6513%;&quot;&gt;&lt;b&gt;&lt;b&gt;LIMIT / OFFSET 사용 방식&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.3022%;&quot;&gt;쿼리 확인&lt;/td&gt;
&lt;td style=&quot;width: 38.8372%;&quot;&gt;OFFSET이 클수록 성능 저하, 커서 기반 페이징 권장&lt;/td&gt;
&lt;td style=&quot;width: 12.2093%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;⭐&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;[Using&amp;nbsp;join&amp;nbsp;buffer&amp;nbsp;(hash&amp;nbsp;join)] &lt;br /&gt;-&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;해시&amp;nbsp;테이블을&amp;nbsp;만듭니다. &lt;br /&gt;-&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;대응&amp;nbsp;레코드를&amp;nbsp;빠르게&amp;nbsp;찾습니다. &lt;br /&gt;-&amp;nbsp;MySQL&amp;nbsp;8.0.18&amp;nbsp;이상에서&amp;nbsp;도입된&amp;nbsp;효율적인&amp;nbsp;조인&amp;nbsp;방식입니다. &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;[Using&amp;nbsp;join&amp;nbsp;buffer&amp;nbsp;(Block&amp;nbsp;nested&amp;nbsp;loop&amp;nbsp;join)] &lt;br /&gt;-&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;블록을&amp;nbsp;순차&amp;nbsp;탐색합니다. &lt;br /&gt;-&amp;nbsp;해시&amp;nbsp;테이블&amp;nbsp;같은&amp;nbsp;구조를&amp;nbsp;사용하지&amp;nbsp;않고&amp;nbsp;단순&amp;nbsp;반복&amp;nbsp;탐색하는&amp;nbsp;방식입니다. &lt;br /&gt;-&amp;nbsp;MySQL&amp;nbsp;5.x&amp;nbsp;버전에서&amp;nbsp;주로&amp;nbsp;사용되었습니다. &lt;br /&gt;&lt;br /&gt;*&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;풀스캔시&amp;nbsp;사용하는&amp;nbsp;조인&amp;nbsp;방법 &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;[Using&amp;nbsp;temporary;&amp;nbsp;Using&amp;nbsp;filesort] &lt;br /&gt;스토리지&amp;nbsp;엔진으로부터&amp;nbsp;받아온&amp;nbsp;레코드를&amp;nbsp;정렬하거나&amp;nbsp;그루핑할&amp;nbsp;때,&amp;nbsp;내부적으로&amp;nbsp;임시&amp;nbsp;테이블&amp;nbsp;사용한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;부가개념&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커넥션 풀을 사용하는 이유?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tcp 커넥션은 신뢰성이 있는 연결을 위해 3-way-handshake와 같은 절차를 수해함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 3-way-handshake로 인한 latency가 발생하기 때문에, 처리량을 향상시키기 위해 연결이 유지되고 있는 커넥션 풀을 사용함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 mysql에서 커넥션은 하나의 스레드에 할당되는 구조이기에 커넥션을 무한정 많이 생성하여 사용하게 되면&lt;br /&gt;쓰레드 또한 무한정 많이 할당되어 컴퓨터 자원을 많이 사용하게 되고 컨텍스트 스위칭으로 인한 오버헤드가 발생하게 되어 오히려 처리량을 떨어트릴 수 있음&lt;b&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>DB/mysql</category>
      <author>코딩공장공장장</author>
      <guid isPermaLink="true">https://developer111.tistory.com/292</guid>
      <comments>https://developer111.tistory.com/entry/RDB-%EB%B6%80%ED%95%98-%EB%B6%84%EC%82%B0-%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94#entry292comment</comments>
      <pubDate>Wed, 8 Oct 2025 12:42:37 +0900</pubDate>
    </item>
    <item>
      <title>[그라파나] 애플리케이션 및 DB 모니터링 지표 분석[1]</title>
      <link>https://developer111.tistory.com/entry/%EA%B7%B8%EB%9D%BC%ED%8C%8C%EB%82%98-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%A7%80%ED%91%9C-%EB%B6%84%EC%84%9D</link>
      <description>&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;스프링 부트 3.2.x&lt;/li&gt;
&lt;li&gt;mysql 8.0&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 pc 환경&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cpu - 16 core&lt;/li&gt;
&lt;li&gt;메모리 32GB&lt;/li&gt;
&lt;/ul&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;동시 접속자 100명&lt;/li&gt;
&lt;li&gt;반복 100회&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;테스트한 api는 GET 요청으로 내부 로직은 select만 존재하며 100건의 게시글을 조회하는 api이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애플리케이션 지표&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;cpu&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oTcxj/btsPKlTEWvx/x8hVKVJMJHBxkoKtXrK4sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oTcxj/btsPKlTEWvx/x8hVKVJMJHBxkoKtXrK4sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oTcxj/btsPKlTEWvx/x8hVKVJMJHBxkoKtXrK4sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoTcxj%2FbtsPKlTEWvx%2Fx8hVKVJMJHBxkoKtXrK4sk%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;982&quot; height=&quot;298&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 cpu 지표를 보면 시스템 cpu 사용량은 99%이지만,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;애플리케이션의 cpu 사용량은 6% 정도이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로컬에서 테스트를 하다보니 크롬, mysql, 도커 등 여러 프레스를 실행중이라 애플리케이션의 cpu 사용량은 높지 않았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정확한 테스트를 수행하기 위해서는 별도의 서버 인스턴스에 애플리케이션만 구동하여 테스트 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메모리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCh1fU/btsPI1hjlUg/Tbiw2paHE9PES8d8Ix47pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCh1fU/btsPI1hjlUg/Tbiw2paHE9PES8d8Ix47pK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCh1fU/btsPI1hjlUg/Tbiw2paHE9PES8d8Ix47pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCh1fU%2FbtsPI1hjlUg%2FTbiw2paHE9PES8d8Ix47pK%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;626&quot; height=&quot;344&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LL11h/btsPKG4s2Mf/TSdznmWkJLGZdryKfYkEZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LL11h/btsPKG4s2Mf/TSdznmWkJLGZdryKfYkEZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LL11h/btsPKG4s2Mf/TSdznmWkJLGZdryKfYkEZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLL11h%2FbtsPKG4s2Mf%2FTSdznmWkJLGZdryKfYkEZk%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;638&quot; height=&quot;344&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qi0S9/btsPKHWDDbJ/V5AUVOQRKJKGvfkmdFj3D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qi0S9/btsPKHWDDbJ/V5AUVOQRKJKGvfkmdFj3D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qi0S9/btsPKHWDDbJ/V5AUVOQRKJKGvfkmdFj3D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqi0S9%2FbtsPKHWDDbJ%2FV5AUVOQRKJKGvfkmdFj3D0%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;626&quot; height=&quot;344&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;메모리 사용량을 보면 old 영역의 지표는 거의 변함이 없고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;young 영역에서만 지표가 요동치는 모습이 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 100명의 사용자로 한명당 100번의 요청을 수행하기에 메모리가 점점 쌓여하는데 오르락 내리락 하는 모습을 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 GC가 불필요한 객체를 제거하기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 STW 지표를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;STW&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1885&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0bn9s/btsPK36czdz/juJ50ssFWFtJMuA0MsvjjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0bn9s/btsPK36czdz/juJ50ssFWFtJMuA0MsvjjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0bn9s/btsPK36czdz/juJ50ssFWFtJMuA0MsvjjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0bn9s%2FbtsPK36czdz%2FjuJ50ssFWFtJMuA0MsvjjK%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;1885&quot; height=&quot;433&quot; data-origin-width=&quot;1885&quot; data-origin-height=&quot;433&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;위와 같이 GC가 불필요한 객체를 제거하는 GC작업이 빈번히 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, GC 작업은 부하가 큰 old 영역이 아닌 young 영역에 집중되는 모습을 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;young 영역의 gc 작업은 stw의 부하 또한 크지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 지표와 같이 수십ms 안에 끝낼 수 있기 때문에, 메모리 지표에서 메모리 용량이 누적되지 않고 제거되는 모습을 보일 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DB 커넥션&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1549&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIBOO7/btsPJWGKFKz/T0iVCNiGJNcZvcg4M0dSxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIBOO7/btsPJWGKFKz/T0iVCNiGJNcZvcg4M0dSxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIBOO7/btsPJWGKFKz/T0iVCNiGJNcZvcg4M0dSxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIBOO7%2FbtsPJWGKFKz%2FT0iVCNiGJNcZvcg4M0dSxk%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;1549&quot; height=&quot;414&quot; data-origin-width=&quot;1549&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100명의 사용자로 테스트를 수행했기에 max-connection size 또한 100개로 설정하였고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시접속하니 100개의 커넥션이 모두 active 상태로 활성화되는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Connection Usage Time&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1863&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4atLD/btsPHYSYUEr/tfkvcO0UCUVgZdSjmdV49K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4atLD/btsPHYSYUEr/tfkvcO0UCUVgZdSjmdV49K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4atLD/btsPHYSYUEr/tfkvcO0UCUVgZdSjmdV49K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4atLD%2FbtsPHYSYUEr%2FtfkvcO0UCUVgZdSjmdV49K%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;1863&quot; height=&quot;380&quot; data-origin-width=&quot;1863&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이 과정에서는 쿼리 결과를 dto 객체에 매핑하는 과정까지 포함된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 로직에서 실행하는 쿼리는 평소 0.0초 대의 실행시간을 갖을만큼 성능이 좋지만, 부하가 발생하니 쿼리의 성능이 느려져&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection usage time이 길어지는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;응답시간&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1870&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xKr0o/btsPHHYBQaI/iyK3HVk0H5TCikHbjox3h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xKr0o/btsPHHYBQaI/iyK3HVk0H5TCikHbjox3h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xKr0o/btsPHHYBQaI/iyK3HVk0H5TCikHbjox3h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxKr0o%2FbtsPHHYBQaI%2FiyK3HVk0H5TCikHbjox3h1%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;1870&quot; height=&quot;294&quot; data-origin-width=&quot;1870&quot; data-origin-height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;응답시간이 최대 7초가 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ms 단위의 api에 부하가 걸리니, 평균 4초, 최대 7초까지의 응답시간이 반환되었다.&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;위의 지표들을 분석해보면, DB 쪽에서 부하가 발생한 듯 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api의 응답이 7초이고, connection usage time 또한 7초 가량 걸리는걸 보면 대부분의 처리가 DB 쪽에서 이뤄지고 지연이 발생한다는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DB - Mysql 지표&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;커넥션&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo8686/btsPKRdICVq/0o6NtaKEwC3rCj2p0FXRXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo8686/btsPKRdICVq/0o6NtaKEwC3rCj2p0FXRXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo8686/btsPKRdICVq/0o6NtaKEwC3rCj2p0FXRXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbo8686%2FbtsPKRdICVq%2F0o6NtaKEwC3rCj2p0FXRXK%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;929&quot; height=&quot;428&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB는 로컬 환경으로 애플리케이션의 커넥션 외에 연결된 것이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connections 지표가 101으로 나오는데, 이는 프로메테우스를 통해 지표를 수집해오는 커넥션이 하나 존재하기에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 Connection 100개와 더해져서 총 101개로 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스레드&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BRaEe/btsPIEtipki/pq28AvKcnkFhsLHrQNos40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BRaEe/btsPIEtipki/pq28AvKcnkFhsLHrQNos40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BRaEe/btsPIEtipki/pq28AvKcnkFhsLHrQNos40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBRaEe%2FbtsPIEtipki%2Fpq28AvKcnkFhsLHrQNos40%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;922&quot; height=&quot;392&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;mysql 또한 스프링 부트와 동일하게 사용자 요청마다 스레드를 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 클라이언트 스레드라고 하는데, 클라이언트 스레드가 101개로 나타나는걸 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Slow Query&lt;img style=&quot;text-align: center; caret-color: transparent; color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot; src=&quot;https://blog.kakaocdn.net/dna/c89ylw/btsPJ3yX4TQ/AAAAAAAAAAAAAAAAAAAAAFIIeps8fJb-ARwmD7z_v0Li3VHtLiIPCazqrCQDDS3N/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1756652399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=q3Lv7RTnnuI6c5A7SSiGYLfSV78%3D&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;375&quot; data-is-animation=&quot;false&quot; /&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slow query의 발생 횟수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slow query의 기준은 DB에서 설정한 기준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 sql을 통해 slow query의 기준을 정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1754473430270&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1.0;&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;또한, 아래와 같은 sql로 slow query 로그 파일의 위치를 확인할 수 있는데&lt;/p&gt;
&lt;pre id=&quot;code_1754473499208&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW VARIABLES LIKE 'slow_query_log_file';&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;로그 파일을 열어 확인하면 아래와 같이 쿼리 실행시간과 탐색한 row수 등을 확인할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;#&amp;nbsp;Time:&amp;nbsp;2025-08-05T05:40:02.021396Z&lt;br /&gt;#&amp;nbsp;User@Host:&amp;nbsp;root[root]&amp;nbsp;@&amp;nbsp;localhost&amp;nbsp;[127.0.0.1]&amp;nbsp;&amp;nbsp;Id:&amp;nbsp;&amp;nbsp;&amp;nbsp;663&lt;br /&gt;# Query_time: 7.338500&amp;nbsp;&amp;nbsp;Lock_time: 0.000005 Rows_sent: 100&amp;nbsp;&amp;nbsp;Rows_examined: 23259&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Query_time이 7.33인걸 보면 이전의 애플리케이션 지표 Connection usage time의 7초가 나왔던 이유가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 실행으로 인해 오래걸렸다는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트래픽&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tJnGw/btsPJvCCnVD/Rk1yGDFwYKHQpyvkhiRFf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tJnGw/btsPJvCCnVD/Rk1yGDFwYKHQpyvkhiRFf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tJnGw/btsPJvCCnVD/Rk1yGDFwYKHQpyvkhiRFf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtJnGw%2FbtsPJvCCnVD%2FRk1yGDFwYKHQpyvkhiRFf0%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;1844&quot; height=&quot;364&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;트래픽 지표는 네트워크를 통한 IO 데이터의 양이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select 요청이기에 Inbound 크기 보다 Outbound 크기가 훨씬 큰 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100건의 목록을 반환하기에 Inbound와 Outbound의 차이가 상당히 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;궁금사항 정리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 동접자 수를 보지 않고 트래픽 지표를 보는가?&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;톰캣은 사용자의 요청마다 스레드를 할당하기에 요청 수의 증가를 스레드를 통해 파악할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꼭 스레드 수를 보지 않더라도 Request Count 지표를 통해서도 확인할 수 있다.&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;그럼에도 불구하고, 왜 트래픽이 대규모 서비스의 대표 지표가 되었을까??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 수와 컴퓨터 자원 사용량은 반드시 비례하지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;내부 처리가 굉장히 많고, 주고 받는 데이터의 크기가 매우 크다면 사용자 수가 적더라도 컴퓨터에 부하를 유발할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 요청 10개보다, 많은 데이터를 주고 받고 복잡한 로직을 갖는 하나의 요청이 더욱 큰 부하가 될 수 있다.&lt;br /&gt;따라서 단순히 많은 요청이 발생했는지가 아니라 요청한 작업의 양까지 포괄하기 위해서 트래픽이라는 지표가 대표지표가 되지 않았나 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(웹서비스는 대부분 DB 처리가 많고, DB에 부하가 집중되기에 DB와 주고받는 데이터의 크기를 대표 지표로 사용하지 않았을까..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB에서 병목이 발생한 이유&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하 테스트 중 평소 0초대의 쿼리가 7초 이상 소요되는 병목 현상이 발생했다.&lt;br /&gt;이에 따라 원인을 다음 세 가지 범주로 나누어 분석하였다.&lt;/p&gt;
&lt;h3 data-end=&quot;211&quot; data-start=&quot;192&quot; data-ke-size=&quot;size23&quot;&gt;1. OS 수준의 원인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;442&quot; data-start=&quot;213&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;334&quot; data-start=&quot;213&quot;&gt;&lt;b&gt;CPU 스케줄링 병목&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;334&quot; data-start=&quot;235&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;288&quot; data-start=&quot;235&quot;&gt;다수의 쿼리가 동시에 실행되며, 제한된 코어 수에서 여러 작업이 순차적으로 처리됨&lt;/li&gt;
&lt;li data-end=&quot;334&quot; data-start=&quot;291&quot;&gt;&amp;rarr; 컨텍스트 스위칭 증가 &amp;rarr; 오버헤드 발생 &amp;rarr; 전체 응답 시간 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;442&quot; data-start=&quot;336&quot;&gt;&lt;b&gt;분석 근거&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;442&quot; data-start=&quot;352&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;389&quot; data-start=&quot;352&quot;&gt;API 응답 시간과 DB 쿼리 실행 시간이 거의 동일&lt;/li&gt;
&lt;li data-end=&quot;442&quot; data-start=&quot;392&quot;&gt;애플리케이션 계층이나 네트워크 지연보다는 DB 실행 자체가 지연된 것으로 판단됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;468&quot; data-start=&quot;449&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;2. DB 수준의 원인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;725&quot; data-start=&quot;470&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;604&quot; data-start=&quot;470&quot;&gt;&lt;b&gt;임시 테이블 생성 (Using temporary)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;604&quot; data-start=&quot;508&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;547&quot; data-start=&quot;508&quot;&gt;정렬, Group By 등으로 인해 메모리 임시 테이블 생성&lt;/li&gt;
&lt;li data-end=&quot;604&quot; data-start=&quot;550&quot;&gt;메모리 한도를 초과하면 &amp;rarr; 디스크 기반 임시 테이블로 &amp;rarr; 디스크 I/O 발생 &amp;rarr; 느려짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;725&quot; data-start=&quot;606&quot;&gt;&lt;b&gt;풀스캔 발생&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;725&quot; data-start=&quot;621&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;650&quot; data-start=&quot;621&quot;&gt;인덱스 부재로 인해 테이블 전체를 스캔&lt;/li&gt;
&lt;li data-end=&quot;693&quot; data-start=&quot;653&quot;&gt;인덱스 부재는 단순히 스캔 범위를 늘리는 것 뿐만 아니라 데이터 페이지가 버퍼 풀에 없으면 디스크 접근 유발&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;753&quot; data-start=&quot;732&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;3. 네트워크 I/O 수준&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;904&quot; data-start=&quot;755&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;820&quot; data-start=&quot;755&quot;&gt;전송해야 할 데이터 양이 많을 경우, 네트워크 처리 대기 시간 증가 가능&lt;/li&gt;
&lt;li data-end=&quot;904&quot; data-start=&quot;821&quot;&gt;하지만 이번 테스트에서는 DB 실행 시간과 API 응답 시간이 유사하기 때문에&lt;br /&gt;&amp;rarr; 네트워크 병목 가능성은 낮은 편으로 판단됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;929&quot; data-start=&quot;911&quot; data-ke-size=&quot;size26&quot;&gt;종합 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1159&quot; data-start=&quot;931&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;983&quot; data-start=&quot;931&quot;&gt;현재 병목의 주 원인은 OS 수준의 CPU 스케줄링과 컨텍스트 스위칭으로 판단된다.&lt;/li&gt;
&lt;li data-end=&quot;1048&quot; data-start=&quot;984&quot;&gt;하지만 쿼리 성능이 애초에 최적화되어 있었다면, 같은 부하에서도 7초까지 느려지지 않았을 가능성이 높다.&lt;/li&gt;
&lt;li data-end=&quot;1159&quot; data-start=&quot;1049&quot;&gt;평소에는 빠르게 처리되던 쿼리도, 풀스캔, 임시 테이블 생성, 인덱스 미사용 등으로 인해&lt;br /&gt;동시 접속자 증가 시 급격한 성능 저하를 유발할 수 있음을 확인했다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>소프트웨어/모니터링</category>
      <author>코딩공장공장장</author>
      <guid isPermaLink="true">https://developer111.tistory.com/286</guid>
      <comments>https://developer111.tistory.com/entry/%EA%B7%B8%EB%9D%BC%ED%8C%8C%EB%82%98-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%A7%80%ED%91%9C-%EB%B6%84%EC%84%9D#entry286comment</comments>
      <pubDate>Tue, 5 Aug 2025 23:41:40 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Kafka 연동] 전송방식 설정부터  에러처리 핸들링까지</title>
      <link>https://developer111.tistory.com/entry/Spring-Kafka-%EC%97%B0%EB%8F%99-%EC%A0%84%EC%86%A1%EB%B0%A9%EC%8B%9D-%EB%B6%80%ED%84%B0-%EC%97%90%EB%9F%AC%EC%B2%98%EB%A6%AC-%ED%95%B8%EB%93%A4%EB%A7%81%EA%B9%8C%EC%A7%80</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;Topic 설정&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Topic 설정은 kafka-UI를 통해 설정하였다.&lt;/span&gt;&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;&lt;span style=&quot;color: #333333;&quot;&gt;partition을 3으로 설정하여 해당 토픽의 메시지를 3개 단위로 나누어 병렬처리가 될 수 있도록 하였다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;(Consumer 설정에서 해당 Topic을 처리하는 Consumer Group의 Consumer를 3개로 해야함, 뒤에서 설명)&lt;/span&gt;&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;&lt;span style=&quot;color: #333333;&quot;&gt;replica.factor는 3으로 설정하여 각 파티션이 3개의 브로커에 복제되도록 하였고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;min.insync.replicas를 2로 설정하여&amp;nbsp;&lt;/span&gt;최소 2개의 브로커에 각 파티션이 복제되면 메시지 전송이 성공하도록 하였다.&lt;/span&gt;&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;&lt;span style=&quot;color: #333333;&quot;&gt;이 조건을 만족할 경우에만 다음 메시지 처리가 가능하도록 설정하였다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;[잠깐] min.insync.replicas&lt;/b&gt;&lt;br /&gt;min.insync.replicas는 Kafka 파티션이 메시지를 정상적으로 처리하기 위해 필요한 최소한의 in-sync replica 수를 설정하는 옵션이다. 이 설정 값만큼의 replica(복제 대상 브로커)가 메시지를 정상적으로 복제해야 해당 메시지가 commit되고, offset이 증가하며 저장이 성공한 것으로 간주된다.&lt;br /&gt;&lt;br /&gt;acks=all 설정과 함께 사용되며, 이 경우 메시지가 min.insync.replicas 수만큼 복제를 완료해야 프로듀서가 성공 응답을 받는다. (acks=1 이나 acks=0일 때는 무시된다.)&lt;br /&gt;만약 조건이 충족되지 않으면, Kafka는 설정된 request.timeout.ms 시간 동안 응답을 대기하며, 이 시간 안에 복제가 완료되지 않으면 메시지 전송은 실패하고 예외가 발생한다.&lt;br /&gt;&lt;br /&gt;min.insync.replicas 값을 너무 높게 설정하면, 복제 시간으로 인해, 처리시간이 delay되어 처리량 저하가 발생할 수 있다. 일반적으로는 2 정도로 설정해도 충분한 신뢰성을 확보할 수 있기 때문에, 운영 환경에서는 min.insync.replicas = 2로 설정하는 경우가 많다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Producer&amp;nbsp; 구현&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;[kafkaTemplate 설정]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753960165392&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class KafkaProducerConfig(
    @Value(&quot;\${spring.kafka.bootstrap-servers}&quot;)
    bootstrapServers: String
) {
    // 문서변환 요청 producer 설정
    private val convertProps = mapOf(
        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
        // 응답 성공 여부 구분
        ProducerConfig.ACKS_CONFIG to &quot;all&quot;,               // 리더+팔로워 복제 성공 후 응답
        // 중복 허용 여부
        ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG to false,
        // 배치
        ProducerConfig.BATCH_SIZE_CONFIG to 16000,         // 배치 전송 크기
        ProducerConfig.LINGER_MS_CONFIG to 1000,           // 최대 대기 시간
        ProducerConfig.BUFFER_MEMORY_CONFIG to 33554432,   // 프로듀서 내부 버퍼 크기
        // 재처리 설정
        ProducerConfig.RETRIES_CONFIG to 3,                // 재시도 횟수
        ProducerConfig.RETRY_BACKOFF_MS_CONFIG to 2000,     // 재시도간 대기 시간
        ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG to 2000,  // 단일 요청에 대한 응답 최대 대기 시간
        ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG to 15000, // 전체 전송 제한 시간
        // 직렬화
        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java.name,
        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java.name,
        // 압축 타입 설정
        ProducerConfig.COMPRESSION_TYPE_CONFIG to &quot;lz4&quot;
    )

    @Bean
    fun kafkaJsonTemplate(): KafkaTemplate&amp;lt;String, JsonToHwpRequestEvent&amp;gt; {
        val producerFactory = DefaultKafkaProducerFactory&amp;lt;String, JsonToHwpRequestEvent&amp;gt;(convertProps)
        return KafkaTemplate(producerFactory)
    }

    @Bean
    fun kafkaHwpTemplate(): KafkaTemplate&amp;lt;String, HwpToHtmlRequestEvent&amp;gt; {
        val producerFactory = DefaultKafkaProducerFactory&amp;lt;String, HwpToHtmlRequestEvent&amp;gt;(convertProps)
        return KafkaTemplate(producerFactory)
    }
}&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;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;[Producer 구현]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Primary
@Component
class HwpConvertEventKafkaAdapter(
    private val kafkaJsonTemplate: KafkaTemplate&amp;lt;String, JsonToHwpRequestEvent&amp;gt;,
    private val kafkaHwpTemplate: KafkaTemplate&amp;lt;String, HwpToHtmlRequestEvent&amp;gt;,
    private val hwpConvertProducerCallback: HwpConvertProducerCallback
) : HwpConvertEventPort {

    override fun requestHwp(eventDto: JsonToHwpRequestEvent) {
        val future = kafkaJsonTemplate.send(JsonToHwpRequestTopic, eventDto.id.toString(), eventDto)
        hwpConvertProducerCallback.attach(future, JsonToHwpRequestTopic, eventDto.id, eventDto)
    }

    override fun requestHtml(eventDto: HwpToHtmlRequestEvent) {
        val future = kafkaHwpTemplate.send(HwpToHtmlRequestTopic, eventDto.id.toString(), eventDto)
        hwpConvertProducerCallback.attach(future, JsonToHwpRequestTopic, eventDto.id, eventDto)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;전송방식&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;kafka Producer의 전송 방식은 크게 아래와 같이 나뉜다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;메시지 전송 후 응답 확인 안함(acks=0)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;메시지 전송 후 응답 확인하기(acks=1, acks=all)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;-&amp;gt; 다시, 동기/비동기로 나뉨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;응답을 확인 여부는 데이터 유실 여부이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;응답을 확인하지 않는다면 kafka broker에 정상 저장됬는지 확인을 하지 않는다는 것이기에 데이터가 유실 될 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동기와 비동기는 순서 보장 여부이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;브로커에 전송된 메시지의 저장 여부를 동기로 확인할지, 비동기로 확인할지 여부이다.&lt;/span&gt;&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;&lt;span style=&quot;color: #333333;&quot;&gt;우선, 나의 서비스에서 데이터는 유실되면 안되기에 응답을 확인하기로 하였다.&lt;br /&gt;응답 확인은 acks=1 또는 acks=all으로 사용할 수 있는데,&lt;br /&gt;acks=1일 경우 리더 브로커에만 메시지를 저장하고, acks=all인 경우 ISR에 모두 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;신뢰성을 위해 acks=all으로 설정했고, 이전에 topic 설정을 하며, min.insync.replicas=2로 설정했기에&lt;br /&gt;복제로 인한 성능 지연은 방지하였다.&lt;/span&gt;&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;메시지 전송 방식은 비동기 방식으로 설정하였다.&lt;/p&gt;
&lt;p data-end=&quot;226&quot; data-start=&quot;168&quot; data-ke-size=&quot;size16&quot;&gt;동기 방식은 재처리 로직을 포함하게 되면 성능 지연이 발생하고 전체 처리량도 저하될 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;352&quot; data-start=&quot;232&quot; data-ke-size=&quot;size16&quot;&gt;또한, 메시지 전송은 일반적으로 DB 트랜잭션 등 다른 작업이 모두 성공한 이후에 마지막 단계에서 수행되기 때문에, 전송 실패가 발생해도 Kafka 자체의 통신 오류 외에 다른 원인을 파악하긴 어렵다.&lt;/p&gt;
&lt;p data-end=&quot;447&quot; data-start=&quot;358&quot; data-ke-size=&quot;size16&quot;&gt;게다가, 이미 DB에 관련 정보가 저장되어 있으므로, 배치 작업을 통해 메시지를 재처리하거나 오류를 추적하는 것도 충분히 가능하다고 판단했다.&lt;/p&gt;
&lt;p data-end=&quot;500&quot; data-start=&quot;453&quot; data-ke-size=&quot;size16&quot;&gt;이러한 이유로, 처리량을 극대화하기 위해 비동기 전송 방식을 선택하게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;500&quot; data-start=&quot;453&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;500&quot; data-start=&quot;453&quot; data-ke-size=&quot;size16&quot;&gt;아래&amp;nbsp;코드에서&amp;nbsp;send&amp;nbsp;함수의&amp;nbsp;반환값은&amp;nbsp;future&amp;nbsp;객체이며,&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;500&quot; data-start=&quot;453&quot; data-ke-size=&quot;size16&quot;&gt;이는 Kafka 메시지 전송 결과를 나중에 알림 형태로 받아볼 수 있는 비동기 결과 객체이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;500&quot; data-start=&quot;453&quot; data-ke-size=&quot;size16&quot;&gt;이 future에 콜백 객체를 등록하면, 메시지 전송의 성공 또는 실패에 따라 후속 처리를 비동기적으로 수행할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1754318167167&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val future = kafkaJsonTemplate.send(JsonToHwpRequestTopic, eventDto.id.toString(), eventDto)
hwpConvertProducerCallback.attach(future, JsonToHwpRequestTopic, eventDto.id, eventDto)&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;&lt;span style=&quot;color: #333333;&quot;&gt;이외에도 고려해야할 부분이 하나 더 있다.&lt;br /&gt;&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;데이터 중복 여부&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Consumer application에서 멱등하게 처리할 수 있게 구현한다면 producer가 메시지가 broker에 중복 저장되더라도 큰 문제는 되지 않는다.&lt;br /&gt;나 또한 Consumer application에서 중복 처리를 하더라도 큰 문제는 되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;중복 허용 여부를 설정하는 옵션이 enabled.idempotence이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;true이면 정확히 한번 저장되고, false이면 중복 저장 될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이전에는 false가 디폴트 값이었지만 최근에는 성능 개선이 되면서 true가 default 값이 되었다고 한다.&lt;br /&gt;내부적인 처리가 추가되어 성능 지연이 있었지만 많이 개선되었다고 하고 default 값 또한 바뀌었으므로&lt;br /&gt;나또한 중복이 발생해도 큰 문제가 되지않지만 true를 사용하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;enabled.idempotence&lt;/b&gt;&lt;br /&gt;enabled.idempotence는 producer가 메시지를 중복 없이 정확히 한번 전송(exactly once)하도록 보장하는 옵션이다.&lt;br /&gt;true로 설정하는 경우, 정확히 한번 전송하는데&lt;br /&gt;메시지를 보낼 때, producer id와 메시지의 sequence number를 함께 보낸다.&lt;br /&gt;pid를 통해 producer를 구분하고, sequence number를 통해 메시지 발행 번호를 구분한다.&lt;br /&gt;이 값들이 클라이언트인 producer에도 존재하고, 서버인 kafak broker에도 존재하므로&amp;nbsp;&lt;br /&gt;클라이언트가 동일한 sequence number로 전송하는 경우, 서버가 이미 메시지를 저장했는지 구분할 수 있다.&lt;br /&gt;만약 이미 메시지 저장을 했다면 저장을 생략하고 성공 응답만 반환하여 중복 저장되지 않도록 한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;enabled.idempotence=false인 경우, 데이터 중복 발생 상황&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;1.&amp;nbsp;Producer가&amp;nbsp;메시지를&amp;nbsp;전송함&lt;br /&gt;2.&amp;nbsp;메시지는&amp;nbsp;브로커에&amp;nbsp;저장됨&lt;br /&gt;3.&amp;nbsp;브로커가&amp;nbsp;응답(ack)을&amp;nbsp;보내기&amp;nbsp;전에&amp;nbsp;네트워크&amp;nbsp;장애로&amp;nbsp;ack가&amp;nbsp;producer에&amp;nbsp;도달하지&amp;nbsp;못함&lt;br /&gt;4. Producer는 실패로 판단하고 동일한 메시지를 다시 전송&lt;br /&gt;5. 브로커는 동일한 메시지를 다시 저장함 &amp;rarr; 중복 발생&lt;br /&gt;&lt;br /&gt;&lt;b&gt;정확히 한번 전송을 위해 enabled.idempotent=true 사용시, max.in.flight.requests.per.connection&amp;lt;=5 이하 권장됨&lt;/b&gt;&lt;br /&gt;max.in.flight.requests.per.connection을 쓰더라도 선행하는 요청이 존재하고&lt;br /&gt;선행하는 요청이 실패로 retry를 하면 뒤에 있는 요청 또한 응답 대기 상태에 빠진다.&lt;br /&gt;결국 선행 요청이 최종 실패하면 뒤의 요청도 다 실패하거나 새로운 sequence number를 받아 재시도하여 순서를 보장한다.&lt;br /&gt;이러한 매커니즘이 5이하일 때만, 잘 적용되도록 설계되어있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;enabled.idempotent=true 와 트랜잭션은 다르다.&lt;/b&gt;&lt;br /&gt;enabled.idempotent=true를 통한 중복 없이 정확히 한번 메시지를 전송하는 방식이다.&lt;br /&gt;단일 메시지에 대해서만 적용 되는 개념이므로, 여러 메시지를 원자적으로 처리해주는 트랜잭션과는 다른 개념이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;* max.in.flight.requests.per.connection&lt;/b&gt;&lt;br /&gt;커넥션 하나에서 응답을 받지 않고 최대 요청 가능 수&lt;br /&gt;즉, 하나의 커넥션에서 병렬 요청을 몇개까지 할 것인지 결정하는 옵션&lt;br /&gt;(TCP는 순서를 보장하기에 커넥션 하나에서 병렬처리가 불가하지만 애플리케이션 레벨에서 병렬로 처리함)&lt;br /&gt;&lt;br /&gt;&lt;b&gt;* batch.size와 헷갈리지 말자.&lt;/b&gt;&lt;br /&gt;batch.size는 요청 하나에 메시지를 몇개 담을지 정하는 옵션&lt;br /&gt;max.in.flight.requests.per.connection는 커넥션 하나에 요청을 몇개 병렬로 처리할지 정하는 옵션&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Producer 동기/비동기 전송&lt;/b&gt;&lt;br /&gt;동기로 응답을 확인하는 경우, 실패시 즉시 확인할 수 있다.&lt;br /&gt;비동기의 경우 애플리케이션 처리와 분리되어 비동기 콜백을 통해 재처리 시도 후 확인이 가능하다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;*Producer 비동기 처리시 메시지 순서 바뀔 수 있음&lt;br /&gt;비동기로 처리하는 경우 요청 순서와 응답 순서가 바뀔 수 있다.&lt;br /&gt;응답을 확인해야하는 것 뿐만 아니라 순서 제어가 필요하다면 동기 방식으로 구현해야한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot; data-section-id=&quot;1fj2b2f&quot; data-start=&quot;614&quot; data-end=&quot;631&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;성능 최적화&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;성능 최적화를 진행할 수 있는 기준은 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;632&quot; data-end=&quot;679&quot;&gt;
&lt;li data-start=&quot;632&quot; data-end=&quot;679&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;batch.size, linger.ms&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;632&quot; data-end=&quot;679&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;compression.type&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;batch.size는 요청 하나에 여러개의 메시지를 담아 전송할때, 그 크기를 정하는 옵션이다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;ProducerConfig.BATCH_SIZE_CONFIG to 16000,         // 배치 전송 크기
ProducerConfig.LINGER_MS_CONFIG to 1000,           // 최대 대기 시간
ProducerConfig.BUFFER_MEMORY_CONFIG to 33554432,   // 프로듀서 내부 버퍼 크기&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;batch size가 너무 크면 메모리를 많이 차지하게 되고 batch size만큼 메시지가 찰 때까지 지연이 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이때 무한히 지연되는 것을 방지하기 위해 linger.ms를 통해 최대 대기 시간의 한계를 설정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;batch size가 너무 작으면 네트워크 오버헤드로 지연시간 증가와 처리량 감소를 일으킬 수 있다.&lt;/span&gt;&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;&lt;span style=&quot;color: #333333;&quot;&gt;batch.size를 결정하는 특별한 기준은 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;서비스를 운영하며 batch.size을 조정하며 최적화 옵션값을 찾아나가야한다.&lt;/span&gt;&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;&lt;span style=&quot;color: #333333;&quot;&gt;다만, batch.size를 설정할 때 고려해야할 옵션이 buffer memory의 크기이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;모든 배치는 버퍼에 적재된 이후 kafka로 전송된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;따라서 producer 애플리케이션에서 사용하는 모든 배치의 크기의 합보다 크게 메모리 공간이 확보되어야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;(참고로, 배치 안에 있는 메시지 하나라도 실패하게 되면 배치 안에 있는 모든 메시지는 실패 처리된다.)&lt;/span&gt;&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;&lt;span style=&quot;color: #333333;&quot;&gt;compression.type은 압축 타입을 결정하는 옵션이다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;ProducerConfig.COMPRESSION_TYPE_CONFIG to &quot;lz4&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;압축을 진행하게 되는 경우 네트워크를 통해 전송하는 데이터의 크기를 낮춰 네트워크 지연 시간을 낮추고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;브로커에 저장되는 용량을 낮출 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다만, producer application에서 압축을 처리하기 위한 시간이나 CPU 자원 점유와 같은 오버헤드가 있기에 이를 고려하여 자신의 서비스에 맞는 타입을 설정해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;나의 경우 어느정도 압축률도 좋고 CPU 자원 점유율이 높지 않은 lz4를 사용하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot; data-section-id=&quot;971has&quot; data-start=&quot;758&quot; data-end=&quot;774&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;에러 처리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;kafka 에러처리는 재시도나 retry topic, dead letter와 같은 방식을 고려할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;일반적으로 producer에서는 재시도 정도만 진행하고,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;retry topic이나 dead letter는 발행한 메시지를 소비하는 Consumer 쪽에서 진행한다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;kafka를 통해 구현하는 기능들이 결과적 일관성을 지키기 위한 목적이 크다 보니 consumer 쪽에서 보다 많은 에러 핸들링을 하는 것 같다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;retry topic이나 dead letter는 consumer 에러 처리에서 설명하겠다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;kafka에서 재처리 시도를 하는 경우, backoff라는 개념이 사용된다.&lt;br /&gt;일시적인 장애로 브로커가 통신을 진행하지 못하는 경우, 재시도 사이에 어느정도 대기 시간을 주어 브로커가 회복할 수 있는 시간을 주는 것이다.&lt;br /&gt;나 또한 2초 정도 대기하고 다시 요청하는 방식을 통해 브로커의 회복 시간을 주었다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753960165400&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;ProducerConfig.RETRIES_CONFIG to 3,                // 재시도 횟수
ProducerConfig.RETRY_BACKOFF_MS_CONFIG to 2000,     // 재시도간 대기 시간
ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG to 2000,  // 단일 요청에 대한 응답 최대 대기 시간
ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG to 15000, // 전체 전송 제한 시간&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;그리고 마지막으로 콜백 객체를 통해 성공/실패 처리를 수행하게 되는데,&lt;br /&gt;예외 로그를 찍고, DB에 기록하는 처리를 진행하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1753960781738&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class HwpConvertProducerCallback(
    private val applicationEventPublisher: ApplicationEventPublisher
) {
    private val log = LoggerFactory.getLogger(this::class.java)
    fun &amp;lt;T, V&amp;gt; attach(
        future: CompletableFuture&amp;lt;SendResult&amp;lt;T, V&amp;gt;&amp;gt;,
        topic: String,
        key: Long,
        value: V
    ) {
        future.whenComplete { result, ex -&amp;gt;
            if (ex != null) {
                handleFailure(ex, topic, key)
            } else {
                handleSuccess(key, result)
            }
        }
    }

    private fun handleFailure(ex: Throwable, topic: String, key: Long) {
        // 실패 처리
        applicationEventPublisher.publishEvent(HwpConvertRequestResultEvent(key, false))
        log.error(&quot;[Kafka send fail] topic: $topic, key: $key, exception: $ex&quot;)
    }

    private fun &amp;lt;T, V&amp;gt; handleSuccess(key: Long, result: SendResult&amp;lt;T, V&amp;gt;) {
        // 성공 처리
        applicationEventPublisher.publishEvent(HwpConvertRequestResultEvent(key, true))
        log.info(&quot;[Kafka send success] key: $key, result: $result&quot;)
    }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[참고. 동기 통신 방식]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기로 응답을 확인하는 경우, 아래와 같이 send 호출 이후 get을 통해 응답 결과를 확인할 수 있다.&lt;br /&gt;이때 예외 타입에 따른 처리를 직접 수행할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1754318686921&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    kafkaJsonTemplate.send(JsonToHwpRequestTopic, eventDto.id.toString(), eventDto)
        .get(6, TimeUnit.SECONDS)
} catch (e: TimeoutException) {
    log.error(&quot;Kafka send timeout&quot;, e)
    throw BusinessServerException(&quot;kafka 예외 발생&quot;)
} catch (e: ExecutionException) {
    log.error(&quot;Kafka execution error&quot;, e)
    throw BusinessServerException(&quot;kafka 예외 발생&quot;)
} catch (e: Exception) {
    log.error(&quot;Kafka unknown error&quot;, e)
    throw BusinessServerException(&quot;kafka 예외 발생&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Consumer - 구현 및 에러처리&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer를 구현할 때, 주의해야하는 부분은 메시지 유실 뿐만 아니라 중복 처리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafka Consumer는 정확히 한번 전송이 이뤄지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복이 발생할 수 있다고 가정하고, 멱등한 처리를 하거나 중복 처리 방어로직을 구현해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 uniq한 id 값을 통해 방어로직을 구현한다.&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;Partition 수와 Consumer의 수&lt;/li&gt;
&lt;li&gt;자동 커밋과 수동 커밋&lt;br /&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;에러처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 Consumer application은 python server이므로 python 코드로 설명을 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Patition수와 Consumer 수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 토픽 생성시 partition 수를 3개로 설정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;partition은 메시지를 보관하는 독립적인 단위이므로 하나의 partition은 하나의 consumer가 소비한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(물론, 반대로 consumer 하나가 여러 partition은 담당 가능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer 수가 더 많다면 일하지 않고 노는 Consumer가 생기므로 리소스를 낭비하는 상황이 되며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer 수가 더 적다면 하나의 Consumer가 두개 이상의 partition을 담당해야하므로 처리량이 저하된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Consumer를 partition 수와 맞춰 3개로 설정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 인스턴스 환경이므로 멀티스레드로 아래와 같이 3개의 Consumer를 생성하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1754388645038&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def start_consumer_thread():
    consumer = KafkaConsumer(
        'numberbox.convert.jsonToHwp.request',
        bootstrap_servers=['localhost:19092','localhost:19093','localhost:19094'],
        group_id='numberbox-convert-group',
        enable_auto_commit=False,
        auto_offset_reset='earliest',
        value_deserializer=lambda m: m.decode('utf-8')
    )
    for message in consumer:
        process_message(message, consumer)


# 3개의 쓰레드로 Kafka consumer 실행
for _ in range(3):
    print(&quot;Kafka Consumer Thread 시작&quot;)
    t = threading.Thread(target=start_consumer_thread)
    t.start()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;자동 커밋과 수동 커밋&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 커밋과 수동 커밋을 구분해야하는 이유는 메시지 유실과 중복 처리 발생 가능성 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 커밋은 Consumer의 처리 여부와 상관 없이 일정 시간이 지나면 자동으로 브로커에 커밋을 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋은 메시지를 소비했다고 판단하고 브로커가 오프셋 값을 증가시키는 동작이다.&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;[자동 커밋- 데이터 유실]&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Consumer 메시지 처리 중&lt;/li&gt;
&lt;li&gt;자동 커밋을 통해 브로커에 메시지가 처리됬다고 표시&lt;/li&gt;
&lt;li&gt;Consumer 처리 도중 장애 발생으로 재시작&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Consumer는 메시지를 모두 처리하지 못했음에도 자동 커밋을 통해 메시지를 소비했다고 표기하므로 데이터 유실이 발생한다.&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;[자동/수동 커밋 - 중복 처리]&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Consumer 메시지 처리 후 커밋 요청&lt;/li&gt;
&lt;li&gt;커밋 실패&lt;/li&gt;
&lt;li&gt;Consumer 장애 발생&lt;/li&gt;
&lt;li&gt;Consumer 재시작 후 같은 메시지를 다시 읽음 -&amp;gt; 중복 처리&lt;/li&gt;
&lt;/ol&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;p data-ke-size=&quot;size16&quot;&gt;다만, 개인적인 생각은 커밋 방식을 통해 중복을 제어하는 것보다 중복이 발생한다고 가능하고 애플리케이션에서 방어로직을 구현하는 것이 더 좋다고 생각한다.&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;수동 커밋은 데이터 유실은 발생하지 않으므로 수동 커밋을 사용하게 됬다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시나 커밋에 실패하더라도 다음 메시지의 커밋을 통해 해결되고, 중복 처리를 진행하더라도 큰 문제가 되지 않았기 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754389937051&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def process_message(message, consumer):
    try:
        if message.topic == 'numberbox.convert.jsonToHwp.request':
            print(&quot;토픽 메시지 처리&quot;)
            json_service.convert_json_to_hwp(message.value)
        elif message.topic == 'numberbox.convert.hwpToHtml.request':
            html_service.convert_hwp_to_html(message.value)
        else:
            print(f&quot;Unknown topic: {message.topic}&quot;)
        # 처리 성공 시 비동기 커밋
        consumer.commit_async()

    except (KafkaError, botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
        # 재시도 대상 예외
        print(&quot;retry&quot;)

        retry_topic = 'numberbox.convert.hwpToHtml.retry.request'

        producer.send(retry_topic, key=message.key, value=message.value)
        producer.flush()
        consumer.commit()

    except Exception as e:
        print(&quot;dlq&quot;)
        # 기타 예외 &amp;rarr; 바로 DLQ
        producer.send('numberbox.convert.hwpToHtml.dlq.request', key=message.key, value=message.value, headers=[('reason', str(e).encode())])
        producer.flush()
        consumer.commit()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;에러 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 처리를 위해 retry topic과 dead letter 큐를 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리중 에러가 발생한 메시지에 대해서 해당 컨슈머가 직접 재시도를 하는 게 아닌 다른 컨슈머가 재처리할 수 있도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 구현한 Consumer는 성공/실패 여부와 상관 없이 지속적으로 메시지를 소비하며 새로운 처리를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 처리량을 늘리고, 재시도로 인한 지연시간을 낮출 수 있다.&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;또한, 복구 가능성이 존재하는 예외와 복구 가능성이 존재하지 않는 예외로 구분하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;third party api 연동으로 인한 네트워크 예외의 경우 해당 서버가 복구되면 재시도시 성공할 가능성이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허나, validation 처리에 실패하거나, 데이터 파싱과 같은 오류는 몇번을 처리하더라도 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 네트워크 예외는 retry topic을 통해 다른 consumer를 통해 처리될 수 있도록 하였고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이외의 예외는 곧바로 dead letter 큐로 이동되어 개발자가 수동으로 오류파악을 하고 대응할 수 있도록 처리하였다.&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>소프트웨어/kafka</category>
      <author>코딩공장공장장</author>
      <guid isPermaLink="true">https://developer111.tistory.com/285</guid>
      <comments>https://developer111.tistory.com/entry/Spring-Kafka-%EC%97%B0%EB%8F%99-%EC%A0%84%EC%86%A1%EB%B0%A9%EC%8B%9D-%EB%B6%80%ED%84%B0-%EC%97%90%EB%9F%AC%EC%B2%98%EB%A6%AC-%ED%95%B8%EB%93%A4%EB%A7%81%EA%B9%8C%EC%A7%80#entry285comment</comments>
      <pubDate>Sun, 3 Aug 2025 21:09:28 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Consumer의 동작 원리</title>
      <link>https://developer111.tistory.com/entry/Kafka-Consumer%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-1</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;Consumer&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Consumer는 Kafka Topic에서 메시지를 읽어오는 주체이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Consumer는 반드시 Consumer Group에 속해 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #000000; text-align: start;&quot;&gt;토픽에 쓰여진 메시지를 Pull 방식으로 가져옴&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; text-align: start;&quot;&gt;Partition의 데이터는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; text-align: start;&quot;&gt;동일&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; text-align: start;&quot;&gt;Consumer Group 내에서 단 하나의 Consumer만 읽을 수 있음&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; text-align: start;&quot;&gt;(Consumer Group 내에서 offset을 공유함)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; text-align: start;&quot;&gt;Consumer Group 이 다르면 Partition의 데이터를 여러 Consumer가 읽을 수 있음&lt;br /&gt;-&amp;gt; 하나의 메시지에 다양한 처리 진행 가능&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #000000; text-align: start;&quot;&gt;한 Consumer는 여러 Partition을 담당할 수 있음&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;Consumer Offset 관리&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 컨슈머가 어디까지 메시지를 읽었는지 추적하고, 장애 복구나 재시작 시 중복 처리 없이 메시지를 이어서 처리할 수 있게 하기 위해 메시지의 위치를 나타내는 offset이라는 값을 관리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;82&quot; data-start=&quot;24&quot;&gt;offset 값은 __consumer_offsets이라는 토픽에서 관리된다.&lt;/li&gt;
&lt;li data-end=&quot;82&quot; data-start=&quot;24&quot;&gt;__consumer_offsets 토픽은 Kafka 클러스터 내에 있는 내부 토픽으로, 각 Consumer Group별, 토픽별, 파티션별로 오프셋 정보를 저장한다.&lt;br /&gt;따라서, 장애가 발생해도 해당 그룹이 마지막으로 커밋한 오프셋부터 다시 읽을 수 있어 데이터 손실 없이 처리가 가능하다.&lt;/li&gt;
&lt;li data-end=&quot;294&quot; data-start=&quot;208&quot;&gt;또한, 서로 다른 Consumer Group들은 각각 독립적으로 오프셋을 관리하기 때문에 한 그룹의 오프셋 변경이 다른 그룹에 영향을 미치지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[오프셋]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;249&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G7wpW/btsPzD9QUPa/qWkaKqUHfMecdatM1H9htk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G7wpW/btsPzD9QUPa/qWkaKqUHfMecdatM1H9htk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G7wpW/btsPzD9QUPa/qWkaKqUHfMecdatM1H9htk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG7wpW%2FbtsPzD9QUPa%2FqWkaKqUHfMecdatM1H9htk%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;656&quot; height=&quot;209&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;249&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 109px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 26.8604%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 48.8954%; height: 18px;&quot;&gt;설명&lt;/td&gt;
&lt;td style=&quot;width: 24.1279%; height: 18px;&quot;&gt;관리 주체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 26.8604%; height: 18px;&quot;&gt;&lt;b&gt;Log End Offset (LEO)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 48.8954%; height: 18px;&quot;&gt;파티션에 현재까지 기록된 가장 마지막 메시지의 오프셋&lt;/td&gt;
&lt;td style=&quot;width: 24.1279%; height: 18px;&quot;&gt;브로커&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 26.8604%; height: 18px;&quot;&gt;&lt;b&gt;High Watermark (HW)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 48.8954%; height: 18px;&quot;&gt;모든 ISR(replica)에게 복제 완료된 오프셋 중 가장 큰 값&lt;/td&gt;
&lt;td style=&quot;width: 24.1279%; height: 18px;&quot;&gt;브로커&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 26.8604%; height: 18px;&quot;&gt;&lt;b&gt;Current Position&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 48.8954%; height: 18px;&quot;&gt;현재 Consumer가 다음으로 읽을 오프셋&lt;/td&gt;
&lt;td style=&quot;width: 24.1279%; height: 18px;&quot;&gt;컨슈머&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 26.8604%; height: 37px;&quot;&gt;&lt;b&gt;Last Commit Offset&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 48.8954%; height: 37px;&quot;&gt;Consumer가 Kafka에 마지막으로 커밋한 오프셋 (안정적인 위치)&lt;br /&gt;consumer 장애 발생 후 재시작 될 때, 이어서 읽을 위치로 결정됨&lt;/td&gt;
&lt;td style=&quot;width: 24.1279%; height: 37px;&quot;&gt;__consumer_offsets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;__consumer_offsets에서 관리하는 값은 Last Commit Offset만 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;각각의 값들이 언제 사용되고 변화되는지 알아보겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;먼저, producer가 메시지를 발행하면 로그파일의 마지막줄에 메시지가 추가될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;가장 마지막에 저장된 메시지의 오프셋을 나타내는 Log End Offset의 값이 증가한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;이후 브로커는 ISR 그룹 내의 다른 브로커의 복제 완료 여부를 파악하고 복제가 완료되면 High Watermark를 증가시킨다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;Log End Offset과 &lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;High Watermark는 producer의 메시지 발행과 브로커의 메시지 복제로 인해 증가하는 값이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, consumer는 브로커를 통해 메시지를 읽어올 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer 인스턴스 내부에서 자신이 어느정도 읽었는지 Current Position을 통해 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커를 통해 메시지를 6번까지 가져왔다면 Current Position은 6이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 브로커는 Consumer가 메시지를 가져갔다고 해서 해당 메시지가 소비됬다고 판단하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer가 메시지를 읽었다는 응답을 보내는 commit을 통해서 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer가 메시지를 처리한 이후 commit을 보내줘야 브로커가 __consumer_offset 토픽에 Last Commit Offset을 6으로 증가시키는 처리를 하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 consumer 장애 발생 후 다시 시작할 때, 이어서 읽을 위치를 결정하는데 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Consumer Commit&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Consumer는 메시지를 읽은 위치를 기억하기 위해 offset commit이라는 동작을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 offset을 기반으로 다음 메시지를 어디서부터 읽을지 결정되기 때문에, 정확한 커밋 전략은 Kafka 시스템의 신뢰성과 데이터 무결성에 큰 영향을 미친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 파티션에 대해 내가 읽어들였는다는 것을 표시하기 위해 현재 위치를 업데이트하는 동작을 커밋이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Auto Commit&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 커밋은 Kafka가 일정 주기마다 Consumer가 읽은 offset을 자동으로 저장해주는 방식이다.&lt;/p&gt;
&lt;pre id=&quot;code_1753515378350&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enable.auto.commit=true
auto.commit.interval.ms=5000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정이 적용되면 Consumer는 메시지를 읽고 나서 처리 여부와 상관없이 5초마다 offset을 Kafka에 저장한다.&lt;br /&gt;(비동기로 Consumer 스레드가 아닌 별도의 백그라운드 스레드가 커밋 실행)&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;/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;ex) 장애 발생으로 메시지 손실&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;254&quot; data-start=&quot;197&quot;&gt;메시지 A와 B는 처리 완료하지 않은 상태에서, Kafka가 5초가 되어 오프셋을 커밋&lt;/li&gt;
&lt;li data-end=&quot;298&quot; data-start=&quot;255&quot;&gt;Kafka는 컨슈머가 메시지 A와 B를 처리한 것으로 인식&lt;/li&gt;
&lt;li data-end=&quot;342&quot; data-start=&quot;299&quot;&gt;실제로는 메시지 A와 B 처리 도중에 컨슈머가 장애로 종료&lt;/li&gt;
&lt;li data-end=&quot;342&quot; data-start=&quot;299&quot;&gt;이후 접근에서 A, B 다음 메시지를 처리하기에 A, B는 처리되지 못함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 리밸런싱으로 중복 처리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;532&quot; data-start=&quot;480&quot;&gt;처리 완료 후 커밋이 아직 안 된 상태에서 리밸런싱이 발생하면,&lt;/li&gt;
&lt;li data-end=&quot;579&quot; data-start=&quot;533&quot;&gt;다른 컨슈머가 같은 메시지를 다시 읽어 &lt;b&gt;중복 처리&lt;/b&gt;가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Manual&amp;nbsp;Commit&amp;nbsp;(수동&amp;nbsp;커밋)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수동 커밋은 offset 커밋을 &lt;b&gt;개발자가 직접 제어&lt;/b&gt;하는 방식&lt;/p&gt;
&lt;pre id=&quot;code_1753515398643&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enable.auto.commit=false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 처리 후 아래와 같이 명시적으로 offset을 커밋합니다:&lt;/p&gt;
&lt;pre id=&quot;code_1753514535799&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;consumer.commitSync(); // 또는 commitAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1147&quot; data-start=&quot;1082&quot;&gt;commitSync()는 동기 방식으로 동작하며, 커밋 성공 여부를 확인할 수 있어 &lt;b&gt;신뢰성이 높다.&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1206&quot; data-start=&quot;1148&quot;&gt;commitAsync()는 비동기 방식으로 빠르지만, 실패 여부를 알 수 없다는 단점이 있다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;[커밋 실패시 대응 방법]&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;커밋에 실패하게 되면 중복 처리가 발생할 수 있다.&lt;br /&gt;이러한 경우를 대비하여 중복 처리 방어 로직을 구현해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;유니크 키를 통한 처리 로직을 갖춘다면 방어 로직에 대해서는 구현할 수 있는 방법이 많다고 생각한다.&lt;br /&gt;실무환경에서 컨슈머 애플리케이션은 언제든 중복이 발생하 수 있다고 가정하고 구현한다고 한다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정리&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 93px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 17px;&quot;&gt;&lt;b&gt;Auto Commit&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 17px;&quot;&gt;&lt;b&gt;Manual Commit&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;&lt;b&gt;설정&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;enable.auto.commit=true&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;enable.auto.commit=false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;&lt;b&gt;제어 주체&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;Kafka가 주기적으로 커밋&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;개발자가 직접 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;&lt;b&gt;신뢰성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;낮음 (유실 가능성)&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;높음 (정확한 제어 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;로그 수집, 메트릭 수집&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;결제 처리, 주문 시스템 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;컨슈머 그룹 (Consumer Group)&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-end=&quot;866&quot; data-start=&quot;772&quot; data-ke-size=&quot;size16&quot;&gt;Consumer Group은 같은 Topic을 처리하는 Consumer를 묶는 단위로 병렬 처리와 failover를 제공한다.&lt;/p&gt;
&lt;p data-end=&quot;866&quot; data-start=&quot;772&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;82&quot; data-start=&quot;61&quot; data-ke-size=&quot;size20&quot;&gt;Consumer Group의 특징&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;499&quot; data-start=&quot;84&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;226&quot; data-start=&quot;84&quot;&gt;&lt;b&gt;하나의 그룹은 여러 컨슈머로 구성될 수 있다&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;226&quot; data-start=&quot;121&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;182&quot; data-start=&quot;121&quot;&gt;같은 Consumer Group에 속한 여러 컨슈머가 토픽의 파티션을 나눠 병렬로 처리할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;226&quot; data-start=&quot;186&quot;&gt;하나의 파티션은 하나의 컨슈머에게만 할당돼 중복 처리를 방지한다.&lt;br /&gt;(컨슈머 하나가 여러 파티션 처리 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;376&quot; data-start=&quot;228&quot;&gt;&lt;b&gt;컨슈머 그룹별로 독립적인 오프셋 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;376&quot; data-start=&quot;261&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;326&quot; data-start=&quot;261&quot;&gt;각 그룹은 자신만의 오프셋 정보를 가지고 있어, 같은 토픽을 여러 그룹이 각각 독립적으로 소비할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;499&quot; data-start=&quot;378&quot;&gt;&lt;b&gt;리밸런싱 지원&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;499&quot; data-start=&quot;398&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;457&quot; data-start=&quot;398&quot;&gt;컨슈머가 늘어나거나 줄어들면 코디네이터(브로커)가 자동으로 파티션을 재분배(리밸런싱)해서 부하를 조절한다.&lt;/li&gt;
&lt;li data-end=&quot;499&quot; data-start=&quot;461&quot;&gt;이 과정에서 오프셋을 참고해 메시지 중복 없이 처리를 이어갑니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-end=&quot;530&quot; data-start=&quot;506&quot; data-ke-size=&quot;size20&quot;&gt;Consumer Group의 존재 이유&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;751&quot; data-start=&quot;532&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;603&quot; data-start=&quot;532&quot;&gt;&lt;b&gt;확장성 확보&lt;/b&gt;&lt;br /&gt;여러 컨슈머가 협력해 메시지를 병렬로 처리할 수 있으므로, 처리량이 증가해도 대응할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;676&quot; data-start=&quot;605&quot;&gt;&lt;b&gt;장애대응&lt;br /&gt;&lt;/b&gt;한 컨슈머가 실패해도 다른 컨슈머가 할당받은 파티션을 처리하여 중단 없이 서비스를 지속할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;751&quot; data-start=&quot;678&quot;&gt;&lt;b&gt;독립적인 메시지 처리 지원&lt;/b&gt;&lt;br /&gt;여러 애플리케이션이나 서비스가 같은 토픽의 메시지를 독립적으로 소비할 수 있게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;그룹 코디네이터&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer Group 내의 Consumer는 추가와 삭제가 유연하게 발생할 수 있다는 전제하에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition을 적절하게 분배해야하는 Rebalancing을 필요로한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Consumer에게 Partition을 균등하게 분배하는 작업을 &lt;b&gt;Consumer Rebalancing&lt;/b&gt;이라고한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 Consumer Rebalancing을 진행하는 브로커를 &lt;b&gt;Group Coordinator&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer Rebalancing은 Consumer Group 마다 독립적으로 발생하므로 &lt;br /&gt;Group Coordinator 또한 Consumer Group 마다 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(즉,&amp;nbsp;하나의&amp;nbsp;Kafka&amp;nbsp;브로커가&amp;nbsp;여러&amp;nbsp;Consumer&amp;nbsp;Group의&amp;nbsp;Coordinator가&amp;nbsp;될&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;&lt;b&gt;[consumer group 등록 및 파티션 할당 과정]&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;consumer는 bootstrap.brokers 설정값을 통해 broker와 연결&lt;/li&gt;
&lt;li&gt;브로커는 그룹 코디네이터 생성하고 컨슈머에게 응답&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;consumer group id의 해시값 기반으로 그룹 코디네이터가 결정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;컨슈머는 컨슈머 등록 요청(join)을 그룹 코디네이터에게 전달&lt;br /&gt;이때, 가장 먼저 요청을 보내는 컨슈머가 컨슈머 그룹의 리더로 선정&lt;/li&gt;
&lt;li&gt;그룹 코디네이터는 컨슈머 그룹이 구독하는 토픽 파티션 리스트를 리더 컨슈머에게 응답&lt;/li&gt;
&lt;li&gt;리더 컨슈머는 파티션 할당 전략에 따라 그룹 내 컨슈머들에게 파티션 할당한 후 그룹 코디네이터에 전달&lt;/li&gt;
&lt;li&gt;그룹 코디네이터는 해당 정보를 캐싱하고 그룹 내 컨슈머들에게 성공 응답&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[그룹 코디네이터와 consumer의 통신 연결]&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&gt;heatbeat : 컨슈머와 그룹 코디네이터가 주기적트로 하트비트를 주고 받는다.&lt;/li&gt;
&lt;li&gt;session.timeout : 특정 시간 안에 하트비트가 발생하지 않으면 컨슈머를 제거&lt;/li&gt;
&lt;li&gt;poll : 메시지를 가져오는 poll 요청을 통해 consumer가 살아있다고 판단하며 특정 시간 동안 poll 없다면 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그룹 코디네이터와 consumer의 통신 연결은 하트비트와 poll을 통해 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하트비트와 poll이 발생하지 않는다면 그룹 코디네이터는 컨슈머가 죽었다고 판단하여 해당 컨슈머를 컨슈머 그룹에서 제거하고&amp;nbsp;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스태틱 멤버쉽&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'스태틱 멤버쉽' 한국말로 직역하면 고정 회원이라고 이해할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer가 재시작 되면 그룹 코디네이터는 새로운 컨슈머가 합류했다고 판단하여 리밸런싱을 수행한다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스태틱 멤버쉽은 단순히 group.instance.id 설정을 통해 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer에 고정 id를 할당하게 되면 그룹 코디네이터가 기존 멤버라고 판단하여 리밸런싱을 수행하지 않는다.&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;consumer는 일반적으로 leave나 join을 통해 종료와 합류 요청을 하게 되는데 스태틱 멤버쉽을 사용하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 요청을 수행하지 않기에 이 과정에서 리밸런싱이 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 session.timeout은 스태틱 멤버에게도 동일하게 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;session.timeout 내에 재시작을 하지 못한 경우, 리밸런싱을 수행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 session.timeout을 재시작 시간보다 더 길게 설정해야 불필요한 리밸런싱을 피할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;컨슈머 파티션 할당 전략&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-end=&quot;251&quot; data-start=&quot;218&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Range Assignor (레인지 파티션 할당, 기본값)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;353&quot; data-start=&quot;252&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;302&quot; data-start=&quot;252&quot;&gt;파티션 수를 컨슈머 수로 나누어 균등하게 분배하는 방식&lt;/li&gt;
&lt;li data-end=&quot;353&quot; data-start=&quot;303&quot;&gt;파티션 수가 컨슈머 수와 정확히 나누어떨어지지 않으면 불균형이 발생할 수 있음&lt;/li&gt;
&lt;li data-end=&quot;353&quot; data-start=&quot;303&quot;&gt;또한, 토픽별로 파티션 할당이 이뤄지기에 2개 이상의 토픽을 구독하는 consumer group은 불균형이 더욱 심해질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;464&quot; data-start=&quot;434&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. RoundRobin Assignor(라운드로빈 파티션 할당)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;540&quot; data-start=&quot;465&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;504&quot; data-start=&quot;465&quot;&gt;구독하는 모든 파티션을 컨슈머에게 라운드로빈 방식으로 균등 분배&lt;/li&gt;
&lt;li data-end=&quot;504&quot; data-start=&quot;465&quot;&gt;토픽 단위가 아닌 전체 파티션을 라운드 로빈하기 때문에 레인지 파티션 할당보다 균등하게 분배됨(다중 토픽 구독시 유리)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;647&quot; data-start=&quot;621&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Sticky Assignor(스티키 파티션 할당)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;728&quot; data-start=&quot;648&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;687&quot; data-start=&quot;648&quot;&gt;이전 할당 상태를 최대한 유지하면서 균등하게 분배&lt;/li&gt;
&lt;li data-end=&quot;728&quot; data-start=&quot;688&quot;&gt;리밸런싱 시 파티션 이동이 최소화되어 성능과 안정성이 좋아짐&lt;/li&gt;
&lt;li data-end=&quot;728&quot; data-start=&quot;688&quot;&gt;최초 할당은 라운드 로빈 방식, 리밸런싱 과정에서는 기존 컨슈머들의 파티션은 최대한 유지한채 파티션 재할당 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;832&quot; data-start=&quot;775&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Cooperative Sticky Assignor (협력적 스티키 파티션 할당, Kafka 2.4+ 이후 추천)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;932&quot; data-start=&quot;833&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;884&quot; data-start=&quot;833&quot;&gt;점진적 리밸런싱을 지원하여 기존 파티션을 유지하면서 필요한 파티션만 이동시킴&lt;/li&gt;
&lt;li data-end=&quot;932&quot; data-start=&quot;885&quot;&gt;전체 컨슈머 중단 없이 리밸런싱이 가능하여 고가용성 확보에 유리함&lt;/li&gt;
&lt;li data-end=&quot;932&quot; data-start=&quot;885&quot;&gt;기존 리밸런싱은 파티션의 소유권 변경이 동시에 이뤄져야하기에 컨슈머의 동작을 일시 중지해야했다.&lt;br /&gt;Cooperative Sticky 방식은 파티션 재배치가 필요한 컨슈머만 순차적으로 변경되므로 나머지 컨슈머는 중단 없이 계속 메시지를 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서의&amp;nbsp;권장&amp;nbsp;기본값은&amp;nbsp;여전히&amp;nbsp;Range&amp;nbsp;Assignor이지만,&amp;nbsp;실제&amp;nbsp;운영&amp;nbsp;환경에서는&amp;nbsp;Cooperative&amp;nbsp;Sticky&amp;nbsp;Assignor가&amp;nbsp;더&amp;nbsp;적합한&amp;nbsp;경우가&amp;nbsp;많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Consumer가 메시지를 읽어오는 과정&lt;/h4&gt;
&lt;p data-end=&quot;122&quot; data-start=&quot;97&quot; data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;Kafka 클러스터에 연결&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;186&quot; data-start=&quot;123&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;186&quot; data-start=&quot;123&quot;&gt;Consumer는 Kafka 클러스터에 접속하고, 자신이 속한 Consumer Group을 식별한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;228&quot; data-start=&quot;188&quot; data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;파티션 할당 (Partition Assignment)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;335&quot; data-start=&quot;229&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;313&quot; data-start=&quot;229&quot;&gt;Kafka의 Group Coordinator(브로커) 가 해당 Consumer Group의 파티션을 컨슈머들에게 자동으로 분배한다.&lt;/li&gt;
&lt;li data-end=&quot;335&quot; data-start=&quot;314&quot;&gt;이때 리밸런싱이 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;365&quot; data-start=&quot;337&quot; data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;poll() 호출로 메시지 요청&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;489&quot; data-start=&quot;366&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;433&quot; data-start=&quot;366&quot;&gt;Consumer는 주기적으로 poll() 메서드를 호출하여 자신에게 할당된 파티션에서 메시지를 가져온다.&lt;/li&gt;
&lt;li data-end=&quot;433&quot; data-start=&quot;366&quot;&gt;Consumer는 자신에게 할당된 Partition의 Leader에게 직접 요청&lt;/li&gt;
&lt;li data-end=&quot;489&quot; data-start=&quot;434&quot;&gt;이 호출이 없다면 Kafka는 &amp;ldquo;이 컨슈머는 죽었다&amp;rdquo;고 판단하고 리밸런싱을 시작할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;520&quot; data-start=&quot;491&quot; data-ke-size=&quot;size16&quot;&gt;4. &lt;b&gt;Kafka 브로커가 메시지를 전송&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;642&quot; data-start=&quot;521&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;573&quot; data-start=&quot;521&quot;&gt;Consumer가 요청한 시점의 오프셋 위치부터 메시지를 가져와서 버퍼에 채운다.&lt;/li&gt;
&lt;li data-end=&quot;642&quot; data-start=&quot;574&quot;&gt;가져오는 메시지 수는 max.poll.records, fetch.max.bytes 등의 설정에 따라 달라집니다.&lt;br /&gt;너무 작다면 처리량이 떨어질 수 있고, 너무 크다면 처리 시간이 길어질 수도 있다.&lt;br /&gt;(물론 최대 대기시간 설정이 가능하기는 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;681&quot; data-start=&quot;644&quot; data-ke-size=&quot;size16&quot;&gt;5. &lt;b&gt;메시지 처리 (Business Logic 실행)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;740&quot; data-start=&quot;682&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;740&quot; data-start=&quot;682&quot;&gt;Consumer는 받은 메시지를 애플리케이션 로직에 따라 처리(DB 저장, API 호출 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;770&quot; data-start=&quot;742&quot; data-ke-size=&quot;size16&quot;&gt;6. &lt;b&gt;오프셋 커밋 (자동 또는 수동)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;956&quot; data-start=&quot;771&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;956&quot; data-start=&quot;771&quot;&gt;메시지 처리가 끝난 후, Consumer는 오프셋을 커밋하여 &quot;나는 여기까지 처리했다&quot;는 정보를 Kafka에 남긴다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;956&quot; data-start=&quot;846&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;893&quot; data-start=&quot;846&quot;&gt;enable.auto.commit=true면 Kafka가 주기적으로 자동 커밋&lt;/li&gt;
&lt;li data-end=&quot;956&quot; data-start=&quot;896&quot;&gt;false면 애플리케이션에서 commitSync() 또는 commitAsync()로 수동 커밋&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Consumer의 Exactly Once&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 producer의 동작원리 글에서 at most once, at least once, at exactly once를 설명하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer에서는 설명을 생략한 이유는 consumer는 실질적으로 at least once만 사용가능하다고 봐야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer가 트랜잭션을 통해 메시지를 전송하면 컨트롤 메시지라는게 기록되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer가 컨트롤 메시지가 존재하는 메시지만 읽어들이게 되면 정확히 한번 읽기가 가능해보이겠지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이또한 중복이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer는 오직 커밋을 통해 브로커에 메시지 처리의 완료여부를 알리는데 커밋이 실패하고 재시작되면 중복 소비할 수 있게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 한번 읽기가 가능해지려면 producer-broker-consumer가 모두 원자적인 작업을 수행해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허나, kafka 아키텍쳐는 고가용성과 느슨한 결합을 통한 확장성을 지향한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer-broker, consumer-consumer간의 연결만 존재하기에 producer가 정확히 한번 전송하더라도 consumer가 정확히 한번 소비할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 consumer는 중복이 발생할 수 있다고 가정하고 중복 방어 로직을 구현하는 것이 실무에서 권장되는 방식이다.&lt;/p&gt;</description>
      <category>소프트웨어/kafka</category>
      <author>코딩공장공장장</author>
      <guid isPermaLink="true">https://developer111.tistory.com/277</guid>
      <comments>https://developer111.tistory.com/entry/Kafka-Consumer%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-1#entry277comment</comments>
      <pubDate>Fri, 1 Aug 2025 22:54:18 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka 개념] Producer의 동작 원리</title>
      <link>https://developer111.tistory.com/entry/Kafka-%EA%B0%9C%EB%85%90-Producer%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-1</link>
      <description>&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Kafka Producer는 Broker에 메시지를 발행(Publish)하는 클라이언트이다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Producer의 메시지 발행 방식&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-start=&quot;104&quot; data-end=&quot;195&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;메시지 생성&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;121&quot; data-end=&quot;195&quot;&gt;
&lt;li data-start=&quot;121&quot; data-end=&quot;147&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ProducerRecord로 메시지 구성&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;151&quot; data-end=&quot;195&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;(key, value, topic, optional partition) 지정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;197&quot; data-end=&quot;325&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;파티셔너가 파티션 결정&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;220&quot; data-end=&quot;325&quot;&gt;
&lt;li data-start=&quot;220&quot; data-end=&quot;241&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;파티셔너는 클라이언트에 존재&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;220&quot; data-end=&quot;241&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;명시된 파티션이 있으면 그대로 사용&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;245&quot; data-end=&quot;294&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;없다면 key가 있을 경우 &amp;rarr; key.hashCode() % partition 수&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;298&quot; data-end=&quot;325&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;key도 없으면 &amp;rarr; round-robin 방식&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;327&quot; data-end=&quot;482&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;배치 처리 (Batch Accumulator)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;363&quot; data-end=&quot;482&quot;&gt;
&lt;li data-start=&quot;363&quot; data-end=&quot;394&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;같은 파티션의 메시지는&amp;nbsp;&lt;b&gt;batch 단위로 묶음&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;398&quot; data-end=&quot;443&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;설정에 따라 linger.ms, batch.size 조건 만족 시 전송&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;447&quot; data-end=&quot;482&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;목적:&amp;nbsp;&lt;b&gt;latency 줄이고 throughput 증가&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;484&quot; data-end=&quot;577&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;브로커 전송 (send)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;508&quot; data-end=&quot;577&quot;&gt;
&lt;li data-start=&quot;508&quot; data-end=&quot;542&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;네트워크를 통해 파티션의&amp;nbsp;&lt;b&gt;leader 브로커에 전송&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;546&quot; data-end=&quot;577&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;브로커는 메시지를 로그 세그먼트에 저장하고 복제 진행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;579&quot; data-end=&quot;660&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;응답 처리&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;595&quot; data-end=&quot;660&quot;&gt;
&lt;li data-start=&quot;595&quot; data-end=&quot;619&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;acks 옵션에 따라 성공 응답 대기&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;623&quot; data-end=&quot;660&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실패 시 retries, idempotence 적용 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Producer 파티셔너&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;토픽의 메시지는 여러 파티션에 독립적으로 관리된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 메시지가 여러 파티션에 고르게 분배되어야 부하가 집중되지 않고 분산되어 처리될 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메시지를 각 파티션으로 분배하는 역할을 파티셔너가 담당하는데 메시지의 키값을 해싱하여 파티션의 위치를 결정한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1. 라운드 로빈&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;키 값을 지정하지 않는 경우 라운드 로빈 알고리즘을 통해 순차적으로 순환시켜 각 파티션에 분배한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 배치 전송에서 비효율이 발생할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;배치는 각 파티션 단위로 존재하는데, 메시지가 순차적으로 각 배치에 저장되지만 배치 사이즈에 도달하지 못하면 전송되지 못한다.&lt;br /&gt;예를들어, 배치에 메시지가 3개 쌓이면 발송되는 조건에서&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;producer는 메시지 6개를 파티셔너에 전달했지만, 각 파티션의 배치에 2개씩만 존재하여 발송하지 못해 처리 지연이 발생할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2. 스티키 파티셔닝&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;스티키 파티셔닝은 라운드 로빈의 비효율적인 전송을 개선한 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;각 파티션의 배치에 메시지를 먼저 채워서 빠르게 전송하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;라운드 로빈 보다 지연시간이 30% 이상 감소했고,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;kafka 2.4 부터 default로 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Producer의 Batch 처리&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;batch 처리하는 이유&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;batch를 이용하면 메시지를 일괄적으로 전송하기 때문에 replica 처리 로직이 줄어들어 Latency(지연시간)를 방지할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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 data-start=&quot;157&quot; data-end=&quot;198&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;메시지를 하나씩 전송하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;네트워크 요청/응답 오버헤드&lt;/b&gt;가 크다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;199&quot; data-end=&quot;257&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여러 메시지를 한 번에 묶어서 보내면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;네트워크 round-trip 비용&lt;/b&gt;을 줄일 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;258&quot; data-end=&quot;299&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;디스크에도 한 번에 쓰게 되어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;디스크 I/O 성능도 개선&lt;/b&gt;된다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-start=&quot;258&quot; data-end=&quot;299&quot;&gt;메시지를 하나씩 디스크에 접근하여 저장하는 것보다, 한번 접근으로 여러 메시지를 한번에 저장 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.5659%; text-align: center;&quot;&gt;옵션&lt;/td&gt;
&lt;td style=&quot;width: 48.1007%; text-align: center;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.5659%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;batch.size&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 48.1007%;&quot;&gt;- default : 16kb&lt;br /&gt;- batch.size에 도달하면 메시지 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.5659%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;linger.ms&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 48.1007%;&quot;&gt;batch.size에 도달하지 않으면 메시지를 보내지 않기에 처리되지 않고 대기하는 시간이 길어질 수 있다.&lt;br /&gt;이때, linger.ms를 통해 대기시간의 한계를 설정하여 batch.size 만큼 메시지가 도달하지 않더라도 메시지를 발행할 수 있다.&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Message Compression(메시지 압축)&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Kafka Producer가 보내는 메시지를&lt;span&gt;&amp;nbsp;&lt;/span&gt;압축해서 네트워크 대역폭과 저장 공간을 절약하는 기능이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;압축된 메시지는 Kafka 브로커와 클라이언트 간에 전송되고, 브로커는 압축된 상태로 메시지를 저장한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;네트워크를 통해 전송하는 데이터의 크기가 크다면 그만큼의 많은 전송 비용이 필요로 하고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 저장하는 브로커의 디스크 공간도 많이 차지하게 된다.&lt;br /&gt;이때, 압축을 사용하게 된다면 네트워크 전송 비용을 아끼고 브로커 저장공간까지 줄일 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만, 압축을 처리하는 과정이 오버헤드로 작용하여 producer의 처리량을 낮출 수 있으니,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;압축시 CPU 부하, 압축률, 처리 시간 등을 고려하여 적절한 압축 타입을 설정하면된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;압축 설정 옵션은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;compression.type&lt;/span&gt;&lt;span style=&quot;color: #212529; text-align: start;&quot;&gt;&amp;nbsp;이다.&lt;/span&gt;&lt;br /&gt;브로커, produecer, topic에서 각각 적용 가능하며&amp;nbsp;&lt;br /&gt;브로커에서 적용하면 모든 토픽에 적용되고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;producer에서 적용하면 해당 producer,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Topic에 적용하면 해당 토픽에만 적용된다.&lt;br /&gt;&lt;br /&gt;기본값은 producer이기에 producer에서 압축 타입을 결정하면 해당 타입으로 broker에 저장할 수 있다.&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff; text-align: center;&quot;&gt;&lt;b&gt;압축타입&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff; text-align: center;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center;&quot;&gt;&lt;b&gt;none (기본값)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;압축하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center;&quot;&gt;&lt;b&gt;gzip&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;높은 압축률, CPU 부하 중간 정도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center;&quot;&gt;&lt;b&gt;snappy&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;빠른 압축/해제 속도, 중간 압축률, 구글에서 개발&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center;&quot;&gt;&lt;b&gt;lz4&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;매우 빠른 압축/해제 속도, 중간 이상의 압축률&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center;&quot;&gt;&lt;b&gt;zstd&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;높은 압축률과 빠른 속도 (Kafka 2.1+부터 지원)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Producer의 전송방식&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Producer가 메시지를 Broker에 잘 전송했는지 확인하기 위해 acks 옵션을 활용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.9923%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;옵션&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1551%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.0387%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;속도&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.2016%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;유실 가능성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.9458%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사용예시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.9923%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;acks=0&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1551%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- 메시지 보내자마자 성공 간주&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- 클라이언트는 브로커의 응답을 받지 않음&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- At most once(최대 한번) 적용 옵션&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.0387%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;매우 빠름&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.2016%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;매우 높음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.9458%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;로그 수집 등&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;일부 유실 허용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.9923%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;acks=1&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1551%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- 리더 브로커가 메시지 저장 후 응답&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;- At most once(최대 한번)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt; - At least once(최소 한번) &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.0387%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;빠름&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.2016%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;있음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.9458%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;비핵심 로직(알림, 메일)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;일부 유실 허용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.9923%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;acks=-1 or all&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1551%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- min.insync.replicas 설정값 만큼 ISR에 포함된 브로커에 저장 후 응답&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- At least once(최소 한번) &lt;br /&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;- Exactly once(정확히 한번)&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.0387%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;느림&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.2016%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;매우 낮음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.9458%;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;금융, 거래 등&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;데이터 무결성 필수 환경&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;acks=0 인 경우,&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;브로커로부터 메시지가 잘 전송 됬는지 확인하지 않는 at most once(최대 한번) 전송방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;데이터 전송에 실패할 수 있으므로 메시지 발송이 0번 발생할 수 있고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실패하더라도 재시도를 하지 않으므로 중복으로 발송되지는 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;따라서 메시지 발송이 0번 또는 1번으로 at most once(최대 한번) 전송이 이루어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;데이터 유실 가능성은 있으나 중복 전송은 발생하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실무에서 비즈니스 로직에서는 거의 사용되지 않으며, 데이터 유실이 어느정도 허용되는 로그 수집 정도에서만 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;acks=1인 경우,&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브로커로부터 응답을 받아 메시지 전송 성공 여부를 확인을 진행하지만 리더 브로커의 디스크에 저장하고 응답을 반환한다.&lt;br /&gt;이 구조에서는 데이터 유실과 중복이 모두 발생할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;[데이터 유실]&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Producer의 메시지 전송&lt;/li&gt;
&lt;li&gt;leader broker가 자신의 디스크에 저장 후 성공 응답&lt;/li&gt;
&lt;li&gt;leader 장애로 follower가 리더로 승격&lt;/li&gt;
&lt;li&gt;follower에 leader의 메시지가 복제되지 않았으므로 데이터 유실 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;[데이터 중복]&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;acks=all에서 발생하는 상황과 같으므로 acks=all에서 설명하겠다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 응답을 확인하므로 재처리를 통해 실패한 메시지를 재전송할 수 있다.&lt;br /&gt;전체 흐름에서는 데이터 유실이 발생할 수 있지만, producer 입장에서는 장애가 나기전 리더 브로커에 메시지를 최소 한번 전송하므로 at least once(최소 한번 전송)을 보장한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비즈니스의 핵심로직 수행에는 부적합하며 알림이나 문자, 메일 전송 같은 비핵심 로직에 적용하여 어느정도 유실을 허용하는 로직에 사용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;acks=-1 또는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;acks=&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;all&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브로커로부터 응답을 받아 메시지 전송 성공 여부를 확인하고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브로커는 min.insync.replicas로 설정된 값만큼 ISR&lt;span style=&quot;color: #333333;&quot;&gt;에 포함된 브로커에 저장 후 응답한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;리더 브로커에 장애가 발생하더라도 follower에 복제가 되어있으므로 데이터 유실은 발생하지 않는다.&lt;br /&gt;다만, 응답을 확인하므로 전송 실패시 재처리 로직을 통해 데이터 중복이 발생할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;[데이터 중복]&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;producer가 메시지 전송&lt;/li&gt;
&lt;li&gt;broker가 메시지 저장 성공 했지만, 장애 상황 발생으로 응답을 보내지 못함&lt;/li&gt;
&lt;li&gt;producer는 메시지 전송에 실패했다고 판단하여 재전송&lt;/li&gt;
&lt;li&gt;broker가 중복된 메시지 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같은 케이스에서 동일한 메시지가 2번 이상 저장되는 데이터 중복이 발생하게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;신뢰성이 매주 높지만,&lt;/span&gt; 복제로 인해 성능이 낮아질 수 있어 batch 옵션을 함께 사용하거나 Partition을 늘리는 것을 고려할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대규모 서비스에서도 min.insync.replicas=2 정도면 충분히 높은 신뢰성과 가용성을 확보할 수 있다고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실무에서 가장 많이 사용되는 방식이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Producer의 exactly once(정확히 한번)전송&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;acks가 0과 1일 때, 데이터 유실이 발생하여 acks=all을 설정하여 데이터 유실을 해결하였지만&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여전히 데이터 중복 문제에 노출 되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다행히 kafka producer는 데이터 중복 문제도 해결하고 정확히 한번 전송하는 것이 가능하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;acks=all과 함께 enable.idempotence=true로 설정하면 정확히 한번 전송이 가능하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;enable.idempotence를 true로 설정하게 되면&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브로커가 producer에 producer id를 할당하고, producer는 메시지 전송시 sequence number를 함께 전송한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브로커는 pid를 통해 producer를 구분하고, sequence number를 통해 메시지의 순서를 확인할 수 있다.&lt;br /&gt;즉, producer가 저장에 성공한 메시지에 대해 응답을 받지 못해 재시도 하는 상황에서 중복되는 메시지를 전송해도&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브로커는 sequence number를 통해 중복을 파악하여 새롭게 저장시키지 않고 producer에게는 성공 응답만 보내&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;중복 없이 정확히 한번 전송을 보장할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;정확히 한번 전송을 위해 enabled.idempotent=true 사용시, max.in.flight.requests.per.connection&amp;lt;=5 이하 권장됨&lt;/b&gt;&lt;br /&gt;max.in.flight.requests.per.connection을 쓰더라도 선행하는 요청이 존재하고&lt;br /&gt;선행하는 요청이 실패로 retry를 하면 뒤에 있는 요청 또한 응답 대기 상태에 빠진다.&lt;br /&gt;결국 선행 요청이 최종 실패하면 뒤의 요청도 다 실패하거나 새로운 sequence number를 받아 재시도하여 순서를 보장한다.&lt;br /&gt;이러한 매커니즘이 5이하일 때만, 잘 적용되도록 설계되어있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;* max.in.flight.requests.per.connection&lt;/b&gt;&lt;br /&gt;커넥션 하나에서 응답을 받지 않고 최대 요청 가능 수&lt;br /&gt;즉, 하나의 커넥션에서 병렬 요청을 몇개까지 할 것인지 결정하는 옵션&lt;br /&gt;(TCP는 순서를 보장하기에 커넥션 하나에서 병렬처리가 불가하지만 애플리케이션 레벨에서 병렬로 처리함)&lt;br /&gt;&lt;br /&gt;&lt;b&gt;* batch.size와 헷갈리지 말자.&lt;/b&gt;&lt;br /&gt;batch.size는 요청 하나에 메시지를 몇개 담을지 정하는 옵션&lt;br /&gt;max.in.flight.requests.per.connection는 커넥션 하나에 요청을 몇개 병렬로 처리할지 정하는 옵션&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.62em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;Producer Transaction&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;acks=all, enable.idempotence=true를 통해 정확히 한번 전송 할 수 있음을 확인하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 단일 메시지의 정확히 한번 전송을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer의 Transaction은 여러 메시지에 대해 원자적은 처리를 보장한다.&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;kafka에서는 트랜잭션을 적용할 수 있는 트랜잭션 api를 제공하는데 위 설정에서 transaction id를 설정한다면 사용 가능하다.&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;[트랜잭션 처리 과정]&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;트랜잭션 초기 설정&lt;br /&gt;acks=all, enable.idempotence=true 및 transaction id 설정&lt;/li&gt;
&lt;li&gt;init transaction
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;436&quot; data-start=&quot;360&quot;&gt;Producer는 transactional.id를 가지고 Kafka 클러스터에 FindCoordinator 요청을 전송&lt;/li&gt;
&lt;li data-end=&quot;515&quot; data-start=&quot;437&quot;&gt;Kafka는 해당 transactional.id에 대한 Transaction Coordinator 브로커를 찾아서 응답&lt;br /&gt;(transactional.id의 해싱 결과로 클러스터 내 브로커를 트랜잭션 코디네이터로 선정)&lt;/li&gt;
&lt;li data-end=&quot;568&quot; data-start=&quot;516&quot;&gt;그 이후 Producer는 이 Coordinator 브로커와 트랜잭션 상태를 주고 받는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;568&quot; data-start=&quot;516&quot;&gt;initPidRequest를 통해 pid를 할당 받고, 트랜잭션 코디네이터는 Pid와 tid(&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;transaction id&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;)를 매핑함&lt;/li&gt;
&lt;li data-end=&quot;568&quot; data-start=&quot;516&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;pid 에포크를 한단계 올리는 동작을 수행하여, 이전의 동일한 PID 에포크에 대한 쓰기 요청 무시&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;begin transaction
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;producer가 파티션 정보 코디네이터에 전달&lt;/li&gt;
&lt;li&gt;코디네이터가 트랜잭션 상태를 onGoing으로 트랜잭션 로그에 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;메시지 전송
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지에 pid, 에포크, 시퀀스 번호 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;성공 시 커밋, 실패시 롤백
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;prepareCommit 또는 prepareAbort를 트랜잭션 로그에 기록&lt;/li&gt;
&lt;li&gt;메시지 파티션에 control message 전송(커밋 표시)&lt;br /&gt;-&amp;gt; 파티션에 사용자 메시지와 commit을 표시하는 메시지인 2개 쌓임&lt;br /&gt;따라서, 트랜잭션 사용하면 commit을 표시하는 메시지로 인해 offset 1만큼 더 증가함&lt;/li&gt;
&lt;li&gt;트랜잭션 로그에 commited 표시&lt;br /&gt;실패시 aborted 표시, 이를 통해 consumer가 aborted 된 메시지는 읽지 않고 commited된 메시지만 읽을 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위를 통해 컨슈머는 read_commited 옵션을 통해 트랜잭션에 성공한 메시지만 읽어들일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;[참고]&lt;/b&gt;&lt;br /&gt;트랜잭션 코디네이터는 pid와 transactional.id를 매핑하고 해당 트랜잭션 전체를 관리한다.&lt;br /&gt;트랜잭션 상태를 관리하는 트랜잭션 로그는&amp;nbsp; __transaction_state 토픽에 저장&lt;br /&gt;-&amp;gt; __transaction_state 토픽도 파티션으로 나뉘어 있으며 transactional.id의 해싱 값으로 파티션 위치 결정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>소프트웨어/kafka</category>
      <author>코딩공장공장장</author>
      <guid isPermaLink="true">https://developer111.tistory.com/275</guid>
      <comments>https://developer111.tistory.com/entry/Kafka-%EA%B0%9C%EB%85%90-Producer%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-1#entry275comment</comments>
      <pubDate>Thu, 31 Jul 2025 22:10:05 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka 개념] Replication</title>
      <link>https://developer111.tistory.com/entry/Kafka-%EA%B0%9C%EB%85%90-Replication</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka Replication&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카에서 Replication이란 각 메시지들을 여러개로 복제해서 카프카 클러스터 내 브로커들에 분산 시키는 동작을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 Replication 덕분에 하나의 브로커가 종료되더라도 카프카는 안정성을 유지할 수 있다.&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;replication factor 옵션을 통해 복제할 브로커의 수를 지정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;factor 수가 커지면 안정성은 높아지지만 그만큼 브로커 리소스를 많이 사용하게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 테스트나 개발환경은 1, 운영환경에서 로그와 같이 유실이 허용되는 메시지는 2, 유실이 허용되지 않는 메시지는 3으로 설정하는 것이 권장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 서비스에서도 3으로 충분히 안정성을 확보하여 가용된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Replication이 필요한 이유&lt;/b&gt;&lt;br /&gt;카프카(Kafka)는 수많은 데이터 파이프라인의 중심에 위치하며, 메인 허브 역할을 수행한다.&lt;br /&gt;이러한 중앙 허브 시스템에 장애가 발생할 경우, 전체 시스템의 장애로 확산될 위험이 크다.&lt;br /&gt;이를 방지하기 위해 카프카는 Replication(복제) 기능을 제공한다. &lt;br /&gt;클러스터 내 브로커 한두 대에 장애가 발생하더라도, 데이터의 복제본을 사전에 다른 브로커에 저장해 두어 데이터를 보호할 수 있다.&lt;br /&gt;또한 Leader-Follower 구조를 통해, 장애가 발생한 리더 브로커를 빠르게 대체하여 시스템의 안정성과 가용성을 유지할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;leader와 follower&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Topic의 메시지는 Partition 단위로 독립적으로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 각 Parition에 대해 leader 브로커와 follower 브로커를 구분하여 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;leader 브로커만이 메시지를 읽기, 쓰기가 가능하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;follower 브로커는 leader와 통신하며 메시지를 복제 저장하는 처리만 수행하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;leader 브로커 장애시 leader로 승격되어 leader 역할을 수행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Producer는 leader 브로커에 메시지를 전송하고, Consumer 또한 leader 브로커를 통해서만 메시지를 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ISR(In Sync Replica)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISR은 리더와 데이터 동기화가 완료된 브로커들로 구성된 복제 그룹(Replication Group)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISR이라는 그룹의 존재 이유는, 리더 장애시 해당 그룹 내에 있는 팔로워, 즉 동기화가 이루어진 브로커만이 leader로 승격되어 메시지 유실 없이 진행하기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISR 내의 팔로워들은 리더와 데이터 일치를 유지하기 위해 지속적으로 리더의 데이터를 따라가게 되고,&amp;nbsp;&lt;br /&gt;리더는 ISR 내 모든 팔로워가 메시지를 받을 때까지 기다린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 팔로워 브로커의 일시적인 오류로 특정 시간 안에 복제 요청을 하지 않는다면 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;리더는 팔로워에 문제가 발생했다고 판단하여 ISR에서 추방한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;추후 장애에 복구하여 데이터 동기화에 따라온다면 다시 ISR 그룹에 포함될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;복제완료 여부 구분&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 replication.factor=3, min.insync.replicas=2라고 하겠다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;402&quot; data-start=&quot;212&quot;&gt;&lt;b&gt;복제 구성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;402&quot; data-start=&quot;230&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;349&quot; data-start=&quot;230&quot;&gt;replication.factor=3이므로, 하나의 파티션은 총 &lt;b&gt;3개의 복제본&lt;/b&gt;을 가진다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;349&quot; data-start=&quot;296&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;318&quot; data-start=&quot;296&quot;&gt;&lt;b&gt;1개의 리더(Leader)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;349&quot; data-start=&quot;324&quot;&gt;&lt;b&gt;2개의 팔로워(Follower)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;402&quot; data-start=&quot;353&quot;&gt;초기 상태에서 이들 모두가 ISR(In-Sync Replica)에 포함된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;597&quot; data-start=&quot;404&quot;&gt;&lt;b&gt;복제 커밋 조건&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;597&quot; data-start=&quot;425&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;530&quot; data-start=&quot;425&quot;&gt;min.insync.replicas=2 설정에 따라, 커밋되기 위해서는 ISR 내 최소 2개의 복제본(리더 포함)에 복제 완료해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;726&quot; data-start=&quot;599&quot;&gt;&lt;b&gt;복제 커밋&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;726&quot; data-start=&quot;631&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;726&quot; data-start=&quot;631&quot;&gt;이 조건이 충족되면, 리더는 커밋되었다는 표시를 한다.&lt;/li&gt;
&lt;li data-end=&quot;726&quot; data-start=&quot;631&quot;&gt;마지막 커밋 오프셋 위치인 High Water Mark 표기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;p data-ke-size=&quot;size16&quot;&gt;만약, 복제되지 않은 메시지를 읽어갔을 때 어떤 상황이 발생하는지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Topic에 대한 Consumer Group이 2개 이상이 존재하는 상황에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 그룹의 Consumer는 메시지를 처리하고, 다른 그룹은 아직 처리하지 않았다고 해보자.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 kafka는 데이터의 일관성을 위해 ISR 내에서 High Watermark를 통해 복제 완료 여부를 구분한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;528&quot; data-start=&quot;460&quot; data-ke-size=&quot;size16&quot;&gt;복제 완료를 구분하기 위해 High Watermark를 표기한다는 것에 대해서는 알았는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해서는 각 브로커가 자신이 어디까지 복제했는지를 추적하고 기록할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래야 리더가 ISR 내 브로커들의 복제 상태를 정확히 파악하고, HWM을 올바르게 계산할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팔로워 브로커는 이러한 복제 상태를 지속적으로 기록하기 위해,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신의 로컬 디스크에 replication-offset-checkpoint 파일을 사용하여 마지막으로 복제한 오프셋을 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일은 특히 브로커가 재시작될 때, 이전 복제 위치를 기억하고 빠르게 복제를 재개하는 데 중요한 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Replication 동작 과정&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-end=&quot;433&quot; data-start=&quot;412&quot; data-ke-size=&quot;size16&quot;&gt;1. 프로듀서 데이터 전송&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;635&quot; data-start=&quot;435&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;474&quot; data-start=&quot;435&quot;&gt;프로듀서는 해당 파티션의 리더 브로커로 메시지를 전송합니다.&lt;/li&gt;
&lt;li data-end=&quot;635&quot; data-start=&quot;475&quot;&gt;acks 설정에 따라 리더는 어떤 조건에서 응답을 줄지 결정:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;635&quot; data-start=&quot;516&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;545&quot; data-start=&quot;516&quot;&gt;acks=0: 복제 여부 상관없이 응답 안 함&lt;/li&gt;
&lt;li data-end=&quot;574&quot; data-start=&quot;548&quot;&gt;acks=1: 리더만 쓰기 성공하면 응답&lt;/li&gt;
&lt;li data-end=&quot;635&quot; data-start=&quot;577&quot;&gt;acks=all/-1: 리더 + min.insync.replicas 이상 복제되면 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;667&quot; data-start=&quot;642&quot; data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;팔로워 &amp;rarr; 리더로 요청&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;806&quot; data-start=&quot;669&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;806&quot; data-start=&quot;753&quot;&gt;팔로워는 주기적으로 리더에게 Fetch 요청을&amp;nbsp;보내, 새로운 데이터를 가져감.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;840&quot; data-start=&quot;813&quot; data-ke-size=&quot;size16&quot;&gt;3. 복제 완료 판단 및 하이워터마크 설정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1000&quot; data-start=&quot;842&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;892&quot; data-start=&quot;842&quot;&gt;&lt;b&gt;팔로워가 fetch 요청을 보낼 때, 새롭게 읽어갈 offset 위치를 전달한다.&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;892&quot; data-start=&quot;842&quot;&gt;이를통해 leader는 offset 이전까지 follower가 데이터를 읽어갔다고 판단한다.&lt;/li&gt;
&lt;li data-end=&quot;892&quot; data-start=&quot;842&quot;&gt;다른 팔로워 브로커에게도 동일한 offset에 대해 fetch 요청을 받는다면 복제가 완료됬다고 판단하고 commit을 진행한다.&lt;/li&gt;
&lt;li data-end=&quot;944&quot; data-start=&quot;893&quot;&gt;commit을 통해 복제 완료된 오프셋+1으로 High Water Mark(HWM)를 표기한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1026&quot; data-start=&quot;1007&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;4. 팔로워 상태 추적&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1146&quot; data-start=&quot;1028&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1107&quot; data-start=&quot;1028&quot;&gt;각 팔로워는 자신이 어디까지 복제했는지를 로컬 디스크의 replication-offset-checkpoint 파일에 저장합니다.&lt;/li&gt;
&lt;li data-end=&quot;1146&quot; data-start=&quot;1108&quot;&gt;브로커 재시작 시 이 파일을 통해 복제 위치를 복구합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1174&quot; data-start=&quot;1153&quot; data-ke-size=&quot;size16&quot;&gt;5. ISR 유지 및 관리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1332&quot; data-start=&quot;1176&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1257&quot; data-start=&quot;1176&quot;&gt;일정 시간(replica.lag.time.max.ms) 동안 팔로워가 리더와의 복제 속도를 따라가지 못하면, ISR에서 제외됩니다.&lt;/li&gt;
&lt;li data-end=&quot;1295&quot; data-start=&quot;1258&quot;&gt;ISR에 들어오지 못한 팔로워는 리더 승격 대상에서 제외됩니다.&lt;/li&gt;
&lt;li data-end=&quot;1332&quot; data-start=&quot;1296&quot;&gt;팔로워가 리더와 다시 동기화되면 ISR로 재편입됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;&quot;팔로워가 fetch 요청을 보낼 때, 새롭게 읽어갈 offset 위치를 전달한다.&quot;는 부분이 kafka와 다른 메시지 큐와 큰 차이점이다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;Rabbit MQ와 같은 메시지 큐는 fetch 이후 ack 응답을 통해 복제 완료 했음을 알리는데,&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;kafka는 ack 없이 새롭게 읽어갈 offset을 fetch 요청에 함께 전달함으로써, 이전 offset 까지는 동기화가 이루어졌음을 전달한다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;만약에 메시지가 만개라면 Rabbit MQ는 2만개의 요청을 주고 받아야하지만,&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;Kafka는 10001개의 요청으로 복제과정을 진행할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;이러한 처리 방식으로 인해 kafka가 빠른 메시지 처리 성능을 갖추게 된 것이다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;또한 leader 브로커는 메시지 읽고 쓰기와 같은 많은 처리를 해야하므로,&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;leader가 push하는 것이 아닌 follower가 pull하는 방식을 통해 leader 브로커의 부하를 줄였다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size26&quot;&gt;Leader Epoch&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;리더 에포크는 카프카의 파티션들이 복구 동작을 할 때, 메시지의 일관성을 유지하기 위한 용도로 사용된다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;복제 과정에서 리더와 팔로워간 오프셋 요청과 응답을 통해 하이워터마크 값을 올리며 동기화를 진행한다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;팔로워는 리더에 오프셋 요청을 통해 메시지를 가져가며 이때 리더의 HWM의 변화를 감지하여 자신의 HWM도 증가시킨다.&lt;br /&gt;리더는 오프셋 요청 이후에 팔로워가 잘 가져갔다고 판단하여 HWM을 증가시킨다.&lt;br /&gt;팔로워는 리더보다 HWM이 1보다 작게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;이후에 한번 더 요청하여 HWM을 증가시킨다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;팔로워와 리더의 HWM이 같아지려면 리더가 새로운 메시지를 전송 받지 않은 상태에서, 팔로워가 한번 더 오프셋 요청을 하는 경우다.&lt;br /&gt;즉, 리더가 신규 메시지를 받지 않은 상태에서 팔로워의 오프셋 요청이 이뤄져야 비로소 HWM이 같아진다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;팔로워는 늘 리더를 따라가는 입장이기에 신규 메시지가 발행되면 항상 HWM이 작게 된다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;이런한 구조로 인해 리더가 장애가 발생하거나 재시작으로 인해 일시 중단되어 팔로워가 리더로 승격된 경우,&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;팔로워는 오프셋 요청으로 가져온 메시지에 대해 아직 HWM을 증가시키지 못한 경우,&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;해당 메시지는 모두 삭제 시킨다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;메시지 유실이 발생하는 것이다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;재시작하는 상황이거나, 일시적인 장애라면 해당 메시지를 유실하는 것은 큰 손해일 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;따라서, 리더 에포크를 사용하게 되면 메시지를 즉시 삭제하지 않고 이전 리더에게 리더 에포크를 요청하여&amp;nbsp;&lt;br /&gt;리더가 복구 됬다면 이전 처럼 하이워터 마크를 올리는 작업을 수행하여 정상 복제된 메시지임을 판단한다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size26&quot;&gt;Controller&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;leader-follower는 파티션 단위로 선출되는 브로커라면,&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;Controller는 전체 클러스터 내에서 하나의 브로커가 담당한다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;Controller는 주키퍼에 저장된 ISR 리스트 정보를 통해 리더 브로커 장애 발생시, 새로운 리더를 선출하게 된다.&lt;/p&gt;
&lt;p data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1359&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size20&quot;&gt;리더 선출 과정&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;특정 ISR의 리더 브로커 장애 발생&lt;/li&gt;
&lt;li&gt;주키퍼는 특정 ISR의 리더 브로커와 연결이 끊어진 것을 판단함&lt;/li&gt;
&lt;li&gt;컨트롤러는 주키퍼를 통해 ISR의 변화가 일어남을 감지하고, 해당 ISR의 새로운 리더를 선출&lt;/li&gt;
&lt;li&gt;컨트롤러가 주키퍼에 새로운 리더 기록&lt;/li&gt;
&lt;li&gt;모든 브로커에 해당 정보 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 위와 같은 과정을 매우 빠르게 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리더 브로커의 장애가 발생하면 Consumer는 아무런 처리를 하지 못하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Consumer의 재시도 시간 내에 빠르게 처리를 해줘야 높은 가용성을 갖춘 서비스가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행이 kafka는 이러한 과정이 수초 이내에 끝날만큼 빠르게 진행을 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;[참고] 리더 선출과 브로커 파티션 리밸런싱은 별개의 처리&lt;/b&gt;&lt;br /&gt;리더 선출과 브로커 파티션 리밸런싱은 서로 다른 처리이지만 둘다 브로커의 장애로 인해 발생한다는 공통점이 있다.&lt;br /&gt;각 처리가 서로에 영향을 주지는 않지만 브로커 장애로 인해 발생하기 때문에 두 독립적인 처리가 모두 발생한다&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;</description>
      <category>소프트웨어/kafka</category>
      <author>코딩공장공장장</author>
      <guid isPermaLink="true">https://developer111.tistory.com/278</guid>
      <comments>https://developer111.tistory.com/entry/Kafka-%EA%B0%9C%EB%85%90-Replication#entry278comment</comments>
      <pubDate>Tue, 29 Jul 2025 22:02:13 +0900</pubDate>
    </item>
  </channel>
</rss>