<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>TeTedo 개발 일기</title>
    <link>https://diary-blockchain.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 10 May 2026 21:05:17 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>TeTedo.</managingEditor>
    <image>
      <title>TeTedo 개발 일기</title>
      <url>https://tistory1.daumcdn.net/tistory/5382291/attach/c764aab0c787402b8ca29df44f8e32e1</url>
      <link>https://diary-blockchain.tistory.com</link>
    </image>
    <item>
      <title>How to write gas optimized solidity contract?</title>
      <link>https://diary-blockchain.tistory.com/396</link>
      <description>&lt;h1&gt;Gas Optimized Contracts&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I will explain about gas optimization when writing contract code with reference to &lt;a href=&quot;https://nodeguardians.io/campaigns/gas-optimization&quot;&gt;nodeguardians gas optimization campaign&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;You can find the all of codes in &lt;a href=&quot;https://github.com/TeTedo/blog-code/tree/main/gas-optimized-contracts&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Reducing Storage Access&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Accessing contract storage is a very expensive operation.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The opcodes for accessing storage are SLOAD and SSTORE. we can see that &lt;a href=&quot;https://github.com/wolflo/evm-opcodes/blob/main/gas.md#a6-sload&quot;&gt;SLOAD&lt;/a&gt; and &lt;a href=&quot;https://github.com/wolflo/evm-opcodes/blob/main/gas.md#a7-sstore&quot;&gt;SSTORE&lt;/a&gt; are very expensive operations.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Here is an example of a gas-optimized contract.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;contract Sload {
    uint256 public stateVariable;

    function unoptimized(uint256 interation) public {
        for (uint256 i = 0; i &amp;lt; interation; i++) {
            stateVariable += i;
        }
    }

    function optimized(uint256 interation) public {
        uint256 cache = stateVariable;
        for (uint256 i = 0; i &amp;lt; interation; i++) {
            cache += i;
        }

        stateVariable = cache;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;First, let's see the unoptimized function.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;function unoptimized(uint256 interation) public {
    for (uint256 i = 0; i &amp;lt; interation; i++) {
        stateVariable += i;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;This function uses state variable directly in each iteration.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Second, let's see the optimized function.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;function optimized(uint256 interation) public {
    uint256 cache = stateVariable;
    for (uint256 i = 0; i &amp;lt; interation; i++) {
        cache += i;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;This function uses a cache variable to store the state variable and uses it in each iteration.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Predicting the result, the optimized function's gas usage should be less than the unoptimized function's gas usage.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Why? Because the unoptimized function accesses the state variable directly, which is more expensive than using a cache variable.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The unoptimized function uses SLOAD and SSTORE opcodes in each iteration.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;On the other hand, the optimized function uses SLOAD and SSTORE opcodes only once. After that, it uses MLOAD and MSTORE opcodes instead.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.evm.codes/&quot;&gt;EVM.codes&lt;/a&gt; shows the opcodes and their gas usage.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MLOAD and MSTORE opcodes require &quot;3&quot; minimum gas and SLOAD and SSTORE opcodes require &quot;100&quot; minimum gas.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let's see the result.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Here is the test code for the Sload contract.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const measureAverageGas = async (
  func: (iterations: number) =&amp;gt; Promise&amp;lt;ContractTransactionResponse&amp;gt;,
  iterations: number,
  times: number
) =&amp;gt; {
  const txPromises = [];
  for (let i = 0; i &amp;lt; times; i++) {
    txPromises.push(func(iterations));
  }
  const txs = await Promise.all(txPromises);
  const receipts = await Promise.all(
    txs.map((tx) =&amp;gt; tx.wait().then((receipt) =&amp;gt; receipt!.gasUsed))
  );
  return receipts.reduce((a, b) =&amp;gt; a + Number(b), 0) / receipts.length;
};

// Measure unoptimized function
const unoptimizedGas = await measureAverageGas(
  (iterations) =&amp;gt; sload.unoptimized(iterations),
  10,
  10
);

// Measure optimized function
const optimizedGas = await measureAverageGas(
  (iterations) =&amp;gt; sload.optimized(iterations),
  10,
  10
);

// Calculate results
const gasSaved = unoptimizedGas - optimizedGas;
const gasSavedPercentage = (gasSaved / unoptimizedGas) * 100;

// Print results
console.log(`\n=== Gas Comparison (10 iterations, 10 runs) ===`);
console.log(`Unoptimized average gas used: ${unoptimizedGas.toFixed(0)}`);
console.log(`Optimized average gas used: ${optimizedGas.toFixed(0)}`);
console.log(`Gas saved: ${gasSaved.toFixed(0)}`);
console.log(`Gas saving percentage: ${gasSavedPercentage.toFixed(2)}%`);

// Assert optimized uses less gas
expect(optimizedGas).to.be.lessThan(unoptimizedGas);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I suppose that unoptimized function and optimized function call 10 times each.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;And the result is like this.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;=== Gas Comparison (10 iterations, 10 runs) ===
Unoptimized average gas used: 32862
Optimized average gas used: 29167
Gas saved: 3695
Gas saving percentage: 11.24%&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hence, the optimized function uses less gas than the unoptimized function.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Packing Variables&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Similar to memory, storage in the EVM is also segmented into 256-bit slots.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;When we declare a variable, it is stored in a 256-bit slot.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let's see the example of a packed variable.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;There are two types of contracts.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;contract PackedVariables {
    uint128 a; // ┐
    uint128 b; // ┴─ Slot 0

    function readSum() public view returns (uint128) {
        return a + b;
    }
}

contract NoPackedVariables {
    uint256 a; // └─ Slot 0
    uint256 b; // └─ Slot 1

    function readSum() public view returns (uint128) {
        return a + b;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PackedVariables contract will require 1 256-bit storage slot.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoPackedVariables contract will require 2 256-bit storage slots.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In &lt;code&gt;readSum&lt;/code&gt; function, the PackedVariables contract will use 1 SLOAD opcode and 1 ADD opcode.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;On the other hand, the NoPackedVariables contract will use 2 SLOAD opcodes and 1 ADD opcode.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Does this mean, ignoring overflow, packed variable contract is more gas efficient always?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;But it's not true always. Let's see the next example.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;contract PackedVariables {
    uint128 a; // ┐
    uint128 b; // ┴─ Slot 0

    function readA() public view returns (uint128) {
        return a;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;contract NoPackedVariables {
    uint256 a; // └─ Slot 0
    uint256 b; // └─ Slot 1

    function readA() public view returns (uint256) {
        return a;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The PackedVariables contract will require 1 SLOAD opcode and more to devide a and b.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;On the other hand, the NoPackedVariables contract will require just 1 SLOAD.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The NoPackedVariables contract is more gas efficient than the PackedVariables contract in this case.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let's see the result by test code.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;describe(&quot;Gas Comparison&quot;, function () {
  it(&quot;PackedVariables should use less gas than NoPackedVariables in readSum function&quot;, async function () {
    const { packedVariables, noPackedVariables } = await loadFixture(
      deployContract
    );

    const measureAverageGas = async (
      contract: any,
      functionName: string,
      times: number
    ) =&amp;gt; {
      const gasPromises = [];
      for (let i = 0; i &amp;lt; times; i++) {
        gasPromises.push(contract[functionName].estimateGas());
      }
      const gasUsages = await Promise.all(gasPromises);
      return gasUsages.reduce((a, b) =&amp;gt; a + Number(b), 0) / gasUsages.length;
    };

    const packedVariablesGas = await measureAverageGas(
      packedVariables,
      &quot;readSum&quot;,
      10
    );

    const noPackedVariablesGas = await measureAverageGas(
      noPackedVariables,
      &quot;readSum&quot;,
      10
    );

    console.log(`\n=== Gas Comparison (10 runs) ===`);
    console.log(
      `PackedVariables.readSum() average gas: ${packedVariablesGas.toFixed(0)}`
    );
    console.log(
      `NoPackedVariables.readSum() average gas: ${noPackedVariablesGas.toFixed(
        0
      )}`
    );
    console.log(
      `Gas saved: ${(noPackedVariablesGas - packedVariablesGas).toFixed(0)}`
    );

    expect(packedVariablesGas).to.be.lessThan(noPackedVariablesGas);
  });

  it(&quot;NoPackedVariables should use less gas than PackedVariables in readA function&quot;, async function () {
    const { packedVariables, noPackedVariables } = await loadFixture(
      deployContract
    );

    const measureAverageGas = async (
      contract: any,
      functionName: string,
      times: number
    ) =&amp;gt; {
      const gasPromises = [];
      for (let i = 0; i &amp;lt; times; i++) {
        gasPromises.push(contract[functionName].estimateGas());
      }
      const gasUsages = await Promise.all(gasPromises);
      return gasUsages.reduce((a, b) =&amp;gt; a + Number(b), 0) / gasUsages.length;
    };

    const noPackedVariablesGas = await measureAverageGas(
      noPackedVariables,
      &quot;readA&quot;,
      10
    );

    const packedVariablesGas = await measureAverageGas(
      packedVariables,
      &quot;readA&quot;,
      10
    );

    console.log(`\n=== Gas Comparison (10 runs) ===`);
    console.log(
      `NoPackedVariables.readA() average gas: ${noPackedVariablesGas.toFixed(
        0
      )}`
    );
    console.log(
      `PackedVariables.readA() average gas: ${packedVariablesGas.toFixed(0)}`
    );
    console.log(
      `Gas saved: ${(packedVariablesGas - noPackedVariablesGas).toFixed(0)}`
    );

    expect(noPackedVariablesGas).to.be.lessThan(packedVariablesGas);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The result is like this.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;=== Gas Comparison (10 runs) ===
PackedVariables.readSum() average gas: 23926
NoPackedVariables.readSum() average gas: 25789
Gas saved: 1863
      ✔ PackedVariables should use less gas than NoPackedVariables in readSum function

=== Gas Comparison (10 runs) ===
NoPackedVariables.readA() average gas: 23479
PackedVariables.readA() average gas: 23521
Gas saved: 42
      ✔ NoPackedVariables should use less gas than PackedVariables in readA function&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Cleaning Up State&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Contracts can &lt;code&gt;selfdestruct()&lt;/code&gt;, and a contract's storage can be cleared by setting any non-zero slot back to 0.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;History of &lt;code&gt;selfdestruct&lt;/code&gt; Changes&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Before London Hardfork&lt;/b&gt;: &lt;code&gt;selfdestruct()&lt;/code&gt; would destroy contracts and refund gas to the caller. The gas refund was significant.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;London Hardfork &lt;a href=&quot;https://eips.ethereum.org/EIPS/eip-3529&quot;&gt;EIP-3529&lt;/a&gt;&lt;/b&gt;: Gas refunds were removed or significantly reduced. &lt;code&gt;selfdestruct&lt;/code&gt; no longer provides gas refunds.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cancun Hardfork &lt;a href=&quot;https://eips.ethereum.org/EIPS/eip-6780&quot;&gt;EIP-6780&lt;/a&gt;&lt;/b&gt;: &lt;code&gt;selfdestruct&lt;/code&gt; can only destroy contracts if it's called in the same transaction where the contract was created. In all other cases, &lt;code&gt;selfdestruct&lt;/code&gt; only transfers Ether to the beneficiary but does not delete the contract code or storage.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;When &lt;code&gt;selfdestruct&lt;/code&gt; is called in the constructor (same transaction), the contract code is not stored, which means the code storage cost (code size &amp;times; 200 gas/byte) is refunded. This can result in lower deployment gas costs compared to a normal contract.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Then we can guess that selfdestruct contract is cheaper than normal contract.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Below is the test code for comparing gas usage between selfdestruct and normal contract.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;it(&quot;should compare gas usage between selfdestruct and normal contract&quot;, async function () {
  [owner] = await hre.ethers.getSigners();

  const CreateContractFactory = await hre.ethers.getContractFactory(
    &quot;CreateContract&quot;
  );
  const createContract = await CreateContractFactory.deploy();
  await createContract.waitForDeployment();

  const createContractTx = createContract.deploymentTransaction();
  if (!createContractTx) {
    throw new Error(&quot;Deployment transaction not found&quot;);
  }
  const createContractReceipt = await createContractTx.wait();
  const createContractGas = createContractReceipt!.gasUsed;

  const CleaningUpContractFactory = await hre.ethers.getContractFactory(
    &quot;CleaningUpContract&quot;
  );
  const cleaningUp = await CleaningUpContractFactory.deploy(
    await owner.getAddress()
  );
  await cleaningUp.waitForDeployment();

  const cleaningUpTx = cleaningUp.deploymentTransaction();
  if (!cleaningUpTx) {
    throw new Error(&quot;Deployment transaction not found&quot;);
  }
  const cleaningUpReceipt = await cleaningUpTx.wait();
  const cleaningUpGas = cleaningUpReceipt!.gasUsed;

  expect(cleaningUpGas).to.be.lessThan(createContractGas);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;And contract's storage can be cleared by &lt;code&gt;delete&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;contract CleaningUpStorage {
    mapping(uint256 =&amp;gt; address) kittenOwner;
    mapping(uint256 =&amp;gt; string) kittenName;

    mapping(uint256 =&amp;gt; address) catOwner;
    mapping(uint256 =&amp;gt; string) catName;

    function evolveKitten(uint256 id) public {
        catOwner[id] = kittenOwner[id];
        catName[id] = kittenName[id]; // Accessing storage incurs X gas
        delete kittenOwner[id];
        delete kittenName[id]; // Deleting storage refunds Y gas
    }

    function evolveKitten2(uint256 id) public {
        catOwner[id] = kittenOwner[id];
        catName[id] = kittenName[id]; // Accessing storage incurs X gas
        delete kittenOwner[id];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In this case, the &lt;code&gt;evolveKitten&lt;/code&gt; function is more gas efficient than the &lt;code&gt;evolveKitten2&lt;/code&gt; function. Because the &lt;code&gt;evolveKitten&lt;/code&gt; function deletes the storage and refunds the gas more than the &lt;code&gt;evolveKitten2&lt;/code&gt; function does.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Below is the test code for comparing gas usage between &lt;code&gt;evolveKitten&lt;/code&gt; and &lt;code&gt;evolveKitten2&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;it(&quot;should evolveKitten be more gas efficient than evolveKitten2&quot;, async function () {
  const CleaningUpStorage = await hre.ethers.getContractFactory(
    &quot;CleaningUpStorage&quot;
  );
  const cleaningUpStorage = await CleaningUpStorage.deploy();
  await cleaningUpStorage.waitForDeployment();

  const CleaningUpStorage2 = await hre.ethers.getContractFactory(
    &quot;CleaningUpStorage&quot;
  );
  const cleaningUpStorage2 = await CleaningUpStorage2.deploy();
  await cleaningUpStorage2.waitForDeployment();

  const testId = 1;
  const testOwner = await owner.getAddress();
  const testName = &quot;Kitten&quot;;

  await cleaningUpStorage.setKitten(testId, testOwner, testName);
  await cleaningUpStorage2.setKitten(testId, testOwner, testName);

  const evolveKittenGas = await cleaningUpStorage.evolveKitten
    .send(testId)
    .then((tx) =&amp;gt; tx.wait().then((receipt) =&amp;gt; receipt!.gasUsed));
  const evolveKitten2Gas = await cleaningUpStorage2.evolveKitten2
    .send(testId)
    .then((tx) =&amp;gt; tx.wait().then((receipt) =&amp;gt; receipt!.gasUsed));

  expect(evolveKittenGas).to.be.lessThan(evolveKitten2Gas);
});

evolveKitten : 68,115 gas
evolveKitten2 : 69,583 gas&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Hardcoding State Variables&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;State variables that are labeled as &lt;code&gt;immutable&lt;/code&gt; or &lt;code&gt;constant&lt;/code&gt; can be hardcoded in the contract code.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;There are two types of contract's bytecode.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Contract Creation Bytecode&lt;/li&gt;
&lt;li&gt;Runtime Bytecode&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;constant&lt;/code&gt; value is stored in the contract creation bytecode.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;immutable&lt;/code&gt; value is stored in the runtime bytecode.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;When we use &lt;code&gt;constant&lt;/code&gt; or &lt;code&gt;immutable&lt;/code&gt; value, the value is hardcoded in the contract code.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hence, we can save gas by using &lt;code&gt;constant&lt;/code&gt; or &lt;code&gt;immutable&lt;/code&gt; value instead of &lt;code&gt;storage&lt;/code&gt; or &lt;code&gt;memory&lt;/code&gt; value.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;contract myContract {

    uint256 constant a = 100; // Declared as literal
    uint256 immutable b; // Initialized in constructor

    constructor(uint256 _b) {
        b = _b;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;One restriction regarding &lt;code&gt;immutable&lt;/code&gt; variables is that you cannot use them inside a &lt;code&gt;pure&lt;/code&gt; function.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Calldata or Memory Parameters&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Calldata is a cheap read-only data location that stores the arguments of function calls.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;When we use calldata, the data is stored in the calldata memory.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;When we use memory, the data is stored in the memory.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Calldata is cheaper than memory because it is read-only and does not require copying the data to the memory.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;contract Calldata {
    function calldataParameter(uint256[] calldata a) public pure returns (uint256[] calldata) {
        return a;
    }

    function memoryParameter(uint256[] memory a) public pure returns (uint256[] memory) {
        return a;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const calldataParameterGas = await calldata.calldataParameter.estimateGas([
  1, 2, 3,
]);
const memoryParameterGas = await calldata.memoryParameter.estimateGas([
  1, 2, 3,
]);

console.log(`calldata parameter gas: ${calldataParameterGas}`);
console.log(`memory parameter gas: ${memoryParameterGas}`);
expect(calldataParameterGas).to.be.lessThan(memoryParameterGas);

calldata parameter gas: 22910
memory parameter gas: 24523&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Arithmetic Tricks&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bitwise operations are cheaper than arithmetic operations.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;contract ArithmeticTrick {
    function bitwiseAdd(uint256 a) public pure returns (uint256) {
        return a &amp;lt;&amp;lt; 1;
    }

    function arithmeticAdd(uint256 a) public pure returns (uint256) {
        return a * 2;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;it(&quot;should compare gas usage between bitwiseAdd and arithmeticAdd&quot;, async function () {
  const ArithmeticTrick = await hre.ethers.getContractFactory(
    &quot;ArithmeticTrick&quot;
  );
  const arithmeticTrick = await ArithmeticTrick.deploy();
  await arithmeticTrick.waitForDeployment();

  const bitwiseAddGas = await arithmeticTrick.bitwiseAdd.estimateGas(1);
  const arithmeticAddGas = await arithmeticTrick.arithmeticAdd.estimateGas(1);

  console.log(`bitwise add gas: ${bitwiseAddGas}`);
  console.log(`arithmetic add gas: ${arithmeticAddGas}`);
  expect(bitwiseAddGas).to.be.lessThan(arithmeticAddGas);
});

bitwise add gas: 21815
arithmetic add gas: 22036&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Inline Assembly&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In some specific cases, directly using inline assembly can be more gas efficient than using Solidity code.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Custom Errors&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Custom errors are cheaper than revert strings.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;contract A {
    function isAuthorized() public {
        if (msg.sender != owner) {
            revert &quot;Unauthorized&quot;;
        }
    }
}

contract B {

    error unauthorizedError();

    function isAuthorized() public {
        if (msg.sender != owner) {
            revert unauthorizedError();
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Custom errors is only 4 bytes. So it is cheaper than revert strings.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The encoded error would be the first 4 bytes of &lt;code&gt;keccak256(unauthorizedError())&lt;/code&gt;.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Modifier Wrappers&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;When functions use modifiers, the Solidity compiler embeds a copy of the modifier's code into the function's bytecode.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;If the modifier is heavy, it is more gas efficient using a wrapper function instead of a modifier.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;modifier myExpensiveModifier() {
    _myExpensiveModifier();
    _;
}

function _myExpensiveModifier() internal view {
    // do something gas expensive
}

function aFunction() public view myExpensiveModifier() {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;references&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ethereum.org/developers/docs/evm/opcodes/&quot;&gt;Ethereum opcode docs&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nodeguardians.io/campaigns/gas-optimization&quot;&gt;nodeguardians gas optimization campaign&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.evm.codes/&quot;&gt;EVM.codes&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발/BlockChain</category>
      <author>TeTedo.</author>
      <guid isPermaLink="true">https://diary-blockchain.tistory.com/396</guid>
      <comments>https://diary-blockchain.tistory.com/396#entry396comment</comments>
      <pubDate>Tue, 4 Nov 2025 15:50:21 +0900</pubDate>
    </item>
    <item>
      <title>Uniswap v2 core 분석</title>
      <link>https://diary-blockchain.tistory.com/395</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;Markdown 을 그대로 티스토리에 옮겼더니 너무 많이 깨지는데 더 깔끔한 글은 &lt;a href=&quot;https://github.com/TeTedo/blog-code/tree/main/contract-uniswap-v2-core&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃허브&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;h1&gt;uniswap v2 core contract 분석&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uniswap v2 core 에는 크게 factory, pair 2개의 컨트랙트가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컨트랙트들을 코드를 까보며 보려고 한다.&lt;/p&gt;
&lt;h1&gt;1. Factory&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;factory 에서는 흔히 LP 라고 부르는 pair를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 프로토콜 수수료를 받을 주소를 저장하고 있는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈여겨볼 함수는 createPair 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;graph TD
    A[토큰 A, B 입력] --&amp;gt; B[token0, token1 정렬]
    B --&amp;gt; C[바이트코드 준비]
    C --&amp;gt; D[솔트 생성]
    D --&amp;gt; E[CREATE2 실행 - 페어 컨트랙트 생성]
    E --&amp;gt; F[initialize 호출]
    F --&amp;gt; G[매핑에 주소 저장]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 코드이다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;function createPair(address tokenA, address tokenB) external returns (address pair) {
    require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
    (address token0, address token1) = tokenA &amp;lt; tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
    require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
    bytes memory bytecode = type(UniswapV2Pair).creationCode;
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    assembly {
        pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
    }
    IUniswapV2Pair(pair).initialize(token0, token1);
    getPair[token0][token1] = pair;
    getPair[token1][token0] = pair; // populate mapping in the reverse direction
    allPairs.push(pair);
    emit PairCreated(token0, token1, pair, allPairs.length);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세히 라인별로 코드를 따라가 본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(1) token0, token1 정렬&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;(address token0, address token1) = tokenA &amp;lt; tokenB ? (tokenA, tokenB) : (tokenB, tokenA);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개의 토큰 address를 받아서 pair를 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pair의 중복을 막기위해 token을 정렬한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 실제 pair를 배포하는 코드이다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(2) 바이트코드 준비&lt;/h2&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;bytes memory bytecode = type(UniswapV2Pair).creationCode;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 pair의 배포에 필요한 바이트 코드의 메모리 주소를 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이트코드의 첫 32바이트에는 길이를 저장하고 나머지에 실제 데이터를 저장한다고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(3) 솔트 생성&lt;/h2&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;bytes32 salt = keccak256(abi.encodePacked(token0, token1));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;a href=&quot;https://docs.soliditylang.org/en/v0.8.30/abi-spec.html&quot;&gt;solidity 공식문서에서 abi.encodePacked&lt;/a&gt;에 대해 설명을 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;abi.encode 와 abi.encodePacked 두가지가 있는데 abi.encodePacked는 압축으로 32바이트 미만 타입들은 패딩값을 안넣는다고 한다. 하지만 값이 배열일때는 패딩을 유지한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;keccak256 함수는 solidity에서의 기본 해시함수이다. sha256등 &lt;a href=&quot;https://www.geeksforgeeks.org/solidity/what-is-hashing-in-solidity/&quot;&gt;다른 해시함수들&lt;/a&gt;보다 비교적 가스비가 덜든다고 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 배열 요소는 32바이트로 패딩
uint16[2] memory arr = [uint16(0x12), uint16(0x34)];
abi.encodePacked(arr) = 0x00120034&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;salt를 만들어서 create2 할 준비를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;salt에 정렬된 token 주소들을 넣기 때문에 같은 token들을 넣으면 같은 값이 나온다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(4) CREATE2 실행&lt;/h2&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;assembly {
    pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://solidity-kr.readthedocs.io/ko/latest/assembly.html&quot;&gt;solidity 공식문서&lt;/a&gt;의 assembly create2의 설명을 보면 아래와 같은 설명이 있다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;create2(v, p, n, s)

create new contract with code mem[p...(p+n)) at address keccak256(0xff . this . s . keccak256(mem[p...(p+n))) and send v wei and return the new address, where 0xff is a 8 byte value, this is the current contract's address as a 20 byte value and s is a big-endian 256-bit value&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설명은 잘 이해가 안됬는데 그나마 EIP-1014의 &lt;a href=&quot;https://ethervm.io/#F5&quot;&gt;CREATE2 OP CODE(0xF5)&lt;/a&gt;를 보면서 이해가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;addr = new memory[offset:offset+length].value(value)&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create2 함수를 이해하기 위해선 컨트랙트의 바이트 코드의 구조를 이해해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 offset 은 bytecode의 실제 데이터가 시작되는 위치인 32바이트 뒤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;length 는 mload(bytecode) 값인데 mload는 mem[p...(p+32))로 bytecode의 앞 32바이트 즉 길이에 대한 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 pair contract에서는 호출자를 constructor에서 factory로 저장한다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;constructor() public {
    factory = msg.sender;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(5) initialize 호출&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;IUniswapV2Pair(pair).initialize(token0, token1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pair 컨트랙트에서 initialize 함수를 살펴보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
    require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
    token0 = _token0;
    token1 = _token1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;factory에서 호출한게 아니라면 revert 시키고 각 token들을 변수에 할당시킨다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(6) 매핑에 주소 저장&lt;/h2&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;getPair[token0][token1] = pair;
getPair[token1][token0] = pair;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;factory에 token들의 주소를 저장시킨다.&lt;/p&gt;
&lt;h1&gt;2. Pair&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pair 컨트랙트에는 유동성 넣고 빼기, 스왑 기능이 있다. Pair는 LP pool 이라고 생각하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(1) mint&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LP token을 mint 하는 함수이다. 여기서부터 조금 복잡해진다. 마찬가지로 차근차근 분석해보려고 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function mint(address to) external lock returns (uint liquidity) {
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    uint balance0 = IERC20(token0).balanceOf(address(this));
    uint balance1 = IERC20(token1).balanceOf(address(this));
    uint amount0 = balance0.sub(_reserve0);
    uint amount1 = balance1.sub(_reserve1);

    bool feeOn = _mintFee(_reserve0, _reserve1);
    uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
    if (_totalSupply == 0) {
        liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
        _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
    } else {
        liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
    }
    require(liquidity &amp;gt; 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
    _mint(to, liquidity);

    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Mint(msg.sender, amount0, amount1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 getReserves 함수부터 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
    _reserve0 = reserve0;
    _reserve1 = reserve1;
    _blockTimestampLast = blockTimestampLast;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뜬금없는 uint112가 나오는데 가스비 최적화를 위한 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;storage 의 한 슬롯은 256비트로 구성되는데 112, 112, 32 를 모두 더하면 256비트가 되기 때문에 슬롯 1개만을 사용하여 가스비를 절약할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reserve0 과 reserve1은 예외가 있긴 하지만 전에 이 컨트랙트가 들고있던 token0과 token1의 개수라고 생각하면 편했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히는 마지막으로 업데이트된 token0과 token1의 개수이다. mint, burn, swap, sync 함수가 실행될때 업데이트 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트 안되어있으면 초기값은 0이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음은 이 컨트랙트가 가지고 있는 실제 토큰들의 개수를 구한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실제 밸런스에서 전에 가지고 있던 토큰의 개수를 뺀다&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 mintFee를 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mintFee는 프로토콜 수수료인데 uniswap v2 whitepaper에서 2.4 Protocol fee 부분을 참고했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;bool feeOn = _mintFee(_reserve0, _reserve1);

// if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
    address feeTo = IUniswapV2Factory(factory).feeTo();
    feeOn = feeTo != address(0);
    uint _kLast = kLast; // gas savings
    if (feeOn) {
        if (_kLast != 0) {
            uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
            uint rootKLast = Math.sqrt(_kLast);
            if (rootK &amp;gt; rootKLast) {
                uint numerator = totalSupply.mul(rootK.sub(rootKLast));
                uint denominator = rootK.mul(5).add(rootKLast);
                uint liquidity = numerator / denominator;
                if (liquidity &amp;gt; 0) _mint(feeTo, liquidity);
            }
        }
    } else if (_kLast != 0) {
        kLast = 0;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;factory 컨트랙트에 feeTo가 설정되어있다면 가져온다. 이 feeTo 주소는 프로토콜의 수수료라고 생각하면 된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 좀 복잡한 부분을 보겠다. feeTo 가 있는 경우 어떻게 수수료를 챙기는지의 부분이다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;if (_kLast != 0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_kLast는 이전 거래 후의 k값 (reserve0 x reserve1)을 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 상태에서는 kLast = 0 이므로 수수료를 계산할 기준점이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 feeTo를 초기에 설정안했다가 중간에 설정하는 경우 feeTo를 설정한 시점 전까지는 수수료를 계산하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;glsl&quot;&gt;&lt;code&gt;uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_reserve0 와 _reserve1의 값은 swap 함수에서 update를 하는데 rootK 가 rootKLast보다 증가했다는 말은 전체 유동성 풀의 가치가 증가했다는 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 위에있던 kLast변수 할당은 mint와 burn 에서만 적용한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;uint _kLast = kLast;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 매번 swap할때마다 프로토콜에 수수료를 지급하는것보다 모아서 mint나 burn 할때 한번에 수수료를 받기 위해서라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 uniswap v2 whitepaper 에서 수수료를 한번에 받는것과 관련된 문장이다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;Collecting this 0.05% fee at the time of the trade would impose an additional gas cost on
every trade. To avoid this, accumulated fees are collected only when liquidity is deposited
or withdrawn.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 fee를 걷어가는 수학 공식을 코드로 표현했다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;if (rootK &amp;gt; rootKLast) {
    uint numerator = totalSupply.mul(rootK.sub(rootKLast));
    uint denominator = rootK.mul(5).add(rootKLast);
    uint liquidity = numerator / denominator;
    if (liquidity &amp;gt; 0) _mint(feeTo, liquidity);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;white paper를 보고 공식을 차근차근 봐보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$f_{1,2} = 1 - \frac{\sqrt{k_1}}{\sqrt{k_2}}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\sqrt{k_1} = rootKLast$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\sqrt{k_2} = rootK$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\frac{\sqrt{k_1}}{\sqrt{k_2}} = 현재 ;상태 ;대비 ;이전 ;상태의 ;비율$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$f_{1,2} = 1 - \frac{\sqrt{k_1}}{\sqrt{k_2}} = 증가한 ;부분의 ;비율$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 증가한 부분의 비율을 구한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;feeTo가 설정되어있다면 수수료의 $\frac{1}{6}$을 프로토콜 수수료로 가져간다고 되어있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;If the feeTo address is set, the protocol will begin charging a 5-basis-point fee, which is taken as a 1/6 cut of the 30-basis-point fees earned by liquidity providers&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수수료는 LP 토큰을 민팅하는 방식으로 수집된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;증가한 부분 비율의 $\frac{1}{6}$만큼 새로운 LP 토큰을 프로토콜에게 민팅한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$&lt;br /&gt;s_m = 민팅 토큰의 양\&lt;br /&gt;s_1 = total ;supply\&lt;br /&gt;&amp;phi; = \frac{1}{6}&lt;br /&gt;$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$&amp;phi; \times f_{1,2} = \frac{s_m}{s_m + s_1}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체중 민팅토큰의 양의 비율이 k값의 증가 비율의 1/6 이랑 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 구하려는건 수수료로 민팅할 개수인 $s_m$이다.&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;phi; \times f_{1,2})({s_m + s_1})= s_m$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$(&amp;phi; \times f_{1,2})s_m + (&amp;phi; \times f_{1,2}){s_1}= s_m$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$(&amp;phi; \times f_{1,2}){s_1}= s_m - (&amp;phi; \times f_{1,2})s_m$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$(&amp;phi; \times f_{1,2}){s_1}= s_m(1- &amp;phi; \times f_{1,2})$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\frac{(&amp;phi; \times f_{1,2}){s_1}}{(1- &amp;phi; \times f_{1,2})}= s_m$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$&amp;phi; = \frac{1}{6}, f_{1,2} = 1 - \frac{\sqrt{k_1}}{\sqrt{k_2}}$ 를 대입해보면 아래와 같이 식이 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\frac{(\frac{1}{6} \times (1 - \frac{\sqrt{k_1}}{\sqrt{k_2}})){s_1}}{(1- \frac{1}{6} \times (1 - \frac{\sqrt{k_1}}{\sqrt{k_2}}))}= s_m$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\frac{(\frac{1}{6} - \frac{1}{6}\frac{\sqrt{k_1}}{\sqrt{k_2}}){s_1}}{(1- (\frac{1}{6} - \frac{1}{6}\frac{\sqrt{k_1}}{\sqrt{k_2}}))}= s_m$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\frac{(\frac{1}{6} - \frac{1}{6}\frac{\sqrt{k_1}}{\sqrt{k_2}}){s_1}}{(\frac{5}{6} + \frac{1}{6}\frac{\sqrt{k_1}}{\sqrt{k_2}})}= s_m$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 분수에 분자, 분모에 각각 $6\sqrt{k_2}$ 를 곱해주면 다음과 같이 정리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\frac{(\sqrt{k_2} - \sqrt{k_1}){s_1}}{5\sqrt{k_2} + \sqrt{k_1}}= s_m$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 $s_m$ 은 위코드에서 계산한 값과 똑같이 계산되는걸 볼수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;uint numerator = totalSupply.mul(rootK.sub(rootKLast));
uint denominator = rootK.mul(5).add(rootKLast);
uint liquidity = numerator / denominator;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;uint liquidity = numerator / denominator;&lt;/code&gt; 에서 uint는 소수점을 허용하지 때문에 liquidity는 1이상인 값이 나와야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 말로 바꿔보면 k값이 커져도 totalSupply 가 크지 않다면 수수료를 민팅하지 않는다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;if (liquidity &amp;gt; 0) _mint(feeTo, liquidity);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;function _mint(address to, uint value) internal {
    totalSupply = totalSupply.add(value);
    balanceOf[to] = balanceOf[to].add(value);
    emit Transfer(address(0), to, value);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 feeTo가 설정되지 않은 조건을 보겠다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;else if (_kLast != 0) {
    kLast = 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 feeTo가 없지만 이전에는 있었다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음에 다시 feeTo를 설정할때 같은 로직을 적용하기 위해 kLast를 0으로 값을 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mint 함수를 마저 본다면 바뀐 totalSupply를 적용하고 totalSupply에 따라서 조건을 나눈다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 SLOAD라는 스토리지에서 읽는 op code를 실행할때 비용을 아끼기 위해 _totalSupply라는 변수에 totalSupply를 넣는다. 추가로 위의 _mintFee에서 totalSupply가 바뀔수 있기때문이라고도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음은 totalSupply가 0인지 아닌지에 따라서 조건문을 탄다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;if (_totalSupply == 0) {
    liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
    _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
    liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;totalSupply가 0이라면 1000wei 만큼 유동성을 address(0)으로 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 1000wei가 이미 민팅되었으니 1wei를 과도하게 비싸게 만들려면 꽤 많은 금액이 들어가야한다. 동시에 totalSupply가 다시 0이 되는걸 방지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 mint를 실행한 경우이기때문에 유저가 가져갈 LP 토큰은 다음과 같이 계산된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$s_{minted} = \sqrt{x_{deposited} \times y_{deposited}} - 1000$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 초기에 넣은 x, y 값의 비율에 의존하기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;totalSupply가 0이 아니라면 두값중 작은값을 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\frac{amount0 \times totalSupply}{reserve0}; or; \frac{amount1 \times totalSupply}{reserve1}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;token0 이 늘어난 비율과 token1이 늘어난 비율 중 작은값 만큼 totalSupply에서 민팅한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 더 작은값을 선택해야하는지 아래에 예시를 들겠다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;현재 : 10ETH, 40,000USDT  1ETH/4000USDT
추가 : 1ETH, 2,000USDT    1ETH/2000USDT
totalSupply = 1000 개라고 한다면

amount0 x totalSupply / reserve0 = 1 x 1000 / 10 = 100
amount1 x totalSupply / reserve1 = 2000 x 1000 / 40000 = 50

ETH 기준 계산으론 100개 민팅
USDT 기준 계산으론 50개 민팅

즉 더 작은값을 선택하여 공정성과 일관성을 유지하는것이다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 선택한 liquidity 값이 0 이상인지 확인 후 민팅한다&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;require(liquidity &amp;gt; 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민팅 후 실제 balance를 reserve에 업데이트 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;_update(balance0, balance1, _reserve0, _reserve1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_update 함수를 들여다 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;// update reserves and, on the first call per block, price accumulators
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
    require(balance0 &amp;lt;= uint112(-1) &amp;amp;&amp;amp; balance1 &amp;lt;= uint112(-1), 'UniswapV2: OVERFLOW');
    uint32 blockTimestamp = uint32(block.timestamp % 2**32);
    uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
    if (timeElapsed &amp;gt; 0 &amp;amp;&amp;amp; _reserve0 != 0 &amp;amp;&amp;amp; _reserve1 != 0) {
        // * never overflows, and + overflow is desired
        price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
        price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
    }
    reserve0 = uint112(balance0);
    reserve1 = uint112(balance1);
    blockTimestampLast = blockTimestamp;
    emit Sync(reserve0, reserve1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요것도 찬찬히 보겠다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;require(balance0 &amp;lt;= uint112(-1) &amp;amp;&amp;amp; balance1 &amp;lt;= uint112(-1), 'UniswapV2: OVERFLOW');
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;balance들이 112비트로 표시할수 있는 값인지 먼저 체크하고 32비트 기반으로 timestamp를 계산한다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;if (timeElapsed &amp;gt; 0 &amp;amp;&amp;amp; _reserve0 != 0 &amp;amp;&amp;amp; _reserve1 != 0) {
    // * never overflows, and + overflow is desired
    price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
    price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 가격 계산을 위한 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소수점의 오차를 줄이기 위해 UQ112x112 라이브러리를 사용해서 2^112 를 곱한값으로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임스탬프가 32비트이므로 256비트기준으로 224비트가 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$2^{112} \times 2^{112} = 2^{224}$ 이므로 112비트인 reserve 값을 224 비트로 표현해서 소수점의 오차를 줄이는 것이다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터로 넣은 값들을 컨트랙트 스토어에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 mint 함수로 돌아와서&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;feeOn 이 true 라면 kLast 를 컨트랙트 스토어에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 Mint event를 발생시킨다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;emit Mint(msg.sender, amount0, amount1);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(2) burn&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mint를 다 봤으면 burn을 그나마 수월하다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function burn(address to) external lock returns (uint amount0, uint amount1) {
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    address _token0 = token0;                                // gas savings
    address _token1 = token1;                                // gas savings
    uint balance0 = IERC20(_token0).balanceOf(address(this));
    uint balance1 = IERC20(_token1).balanceOf(address(this));
    uint liquidity = balanceOf[address(this)];

    bool feeOn = _mintFee(_reserve0, _reserve1);
    uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
    amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
    amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
    require(amount0 &amp;gt; 0 &amp;amp;&amp;amp; amount1 &amp;gt; 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
    _burn(address(this), liquidity);
    _safeTransfer(_token0, to, amount0);
    _safeTransfer(_token1, to, amount1);
    balance0 = IERC20(_token0).balanceOf(address(this));
    balance1 = IERC20(_token1).balanceOf(address(this));

    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Burn(msg.sender, amount0, amount1, to);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것도 차근차근 보면&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // 이전까지 적용된 토큰의 개수를 받아오고
address _token0 = token0;
address _token1 = token1; // 토큰 컨트랙트 불러오고
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this)); // 실제 컨트랙트가 가지고 있는 수량을 가져온다.

uint liquidity = balanceOf[address(this)]; // 이 컨트랙트의 balance 를 가져오는데 burn 함수를 호출하기전에 이 컨트랙트에 토큰을 전송하는걸로 추측된다.

bool feeOn = _mintFee(_reserve0, _reserve1); // mint 함수와 마찬가지로 수수료를 챙긴다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음은 liquidity 의 개수만큼 토큰을 받는 로직이다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 식은 실제 잔액에서 전체 발행된 liquidity 에서 burn 시키려는 liquidity의 비율만큼 각 토큰을 받는것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 liquidity 만큼의 토큰을 소각시키고 토큰0과 토큰1을 to에게 보내준다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 뒤는 mint 와 동일하게 update 후 kLast를 업데이트 시켜준다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt; _update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(3) swap&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;swap 함수는 토큰을 교환하는 핵심 함수이다. Uniswap V2의 AMM 메커니즘을 구현한 부분으로, constant product formula를 기반으로 거래를 실행한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
    require(amount0Out &amp;gt; 0 || amount1Out &amp;gt; 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    require(amount0Out &amp;lt; _reserve0 &amp;amp;&amp;amp; amount1Out &amp;lt; _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

    uint balance0;
    uint balance1;
    { // scope for _token{0,1}, avoids stack too deep errors
    address _token0 = token0;
    address _token1 = token1;
    require(to != _token0 &amp;amp;&amp;amp; to != _token1, 'UniswapV2: INVALID_TO');
    if (amount0Out &amp;gt; 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
    if (amount1Out &amp;gt; 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
    if (data.length &amp;gt; 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
    balance0 = IERC20(_token0).balanceOf(address(this));
    balance1 = IERC20(_token1).balanceOf(address(this));
    }
    uint amount0In = balance0 &amp;gt; _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
    uint amount1In = balance1 &amp;gt; _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
    require(amount0In &amp;gt; 0 || amount1In &amp;gt; 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
    { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
    uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
    uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
    require(balance0Adjusted.mul(balance1Adjusted) &amp;gt;= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
    }

    _update(balance0, balance1, _reserve0, _reserve1);
    emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차근차근 분석해보겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 기본 검증&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;require(amount0Out &amp;gt; 0 || amount1Out &amp;gt; 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out &amp;lt; _reserve0 &amp;amp;&amp;amp; amount1Out &amp;lt; _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 검증은 출력 토큰 중 하나라도 0보다 큰지 확인한다. 두 번째는 이전에 설명한 getReserves 함수를 사용해서 현재 리저브를 가져온다. 마지막으로 출력 토큰 양이 리저브보다 작은지 확인한다. 이는 유동성이 충분한지 확인하는 검증이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) Optimistic Transfer와 Flash Swap&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 &amp;amp;&amp;amp; to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out &amp;gt; 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out &amp;gt; 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length &amp;gt; 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 Uniswap V2의 핵심 특징 중 하나인 &lt;b&gt;Flash Swap&lt;/b&gt; 기능을 구현한 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 토큰 주소를 로컬 변수에 저장한다. 이는 가스 절약과 스택 깊이 제한 회피를 위한 최적화이다.&lt;/p&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;require(to != _token0 &amp;amp;&amp;amp; to != _token1, 'UniswapV2: INVALID_TO');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 검증은 토큰이 자기 자신에게 전송되는 것을 방지한다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;if (amount0Out &amp;gt; 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out &amp;gt; 0) _safeTransfer(_token1, to, amount1Out);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Optimistic Transfer&lt;/b&gt;라고 불리는 이 방식은 입력 토큰을 받기 전에 출력 토큰을 먼저 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이렇게 설계했냐면 Flash Swap을 지원하기 위해서이다. Flash Swap은 사용자가 출력 토큰을 먼저 받아서 다른 작업을 수행한 후, 나중에 입력 토큰을 반환하는 메커니즘이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;if (data.length &amp;gt; 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 &lt;code&gt;data.length &amp;gt; 0&lt;/code&gt;이면, &lt;code&gt;to&lt;/code&gt; 주소가 &lt;code&gt;IUniswapV2Callee&lt;/code&gt; 인터페이스를 구현한 컨트랙트라는 의미이다. 이 경우 &lt;code&gt;uniswapV2Call&lt;/code&gt; 함수를 호출하여 Flash Swap을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Flash Swap 사용 예시:&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;사용자가 DAI를 받고 싶지만 가지고 있지 않음&lt;/li&gt;
&lt;li&gt;Flash Swap으로 DAI를 먼저 받음&lt;/li&gt;
&lt;li&gt;받은 DAI로 다른 거래 수행&lt;/li&gt;
&lt;li&gt;그 거래의 수익으로 ETH를 얻음&lt;/li&gt;
&lt;li&gt;ETH를 Uniswap에 반환하여 DAI 대금 지불&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;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 실제 입력량 계산&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;uint amount0In = balance0 &amp;gt; _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 &amp;gt; _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In &amp;gt; 0 || amount1In &amp;gt; 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 실제로 입력된 토큰 양을 계산한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;_reserve0 - amount0Out&lt;/code&gt;: 출력 후 예상 리저브&lt;/li&gt;
&lt;li&gt;&lt;code&gt;balance0 &amp;gt; _reserve0 - amount0Out&lt;/code&gt;: 실제 잔액이 예상 리저브보다 큰지 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;balance0 - (_reserve0 - amount0Out)&lt;/code&gt;: 차이만큼이 입력량&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) K 값 검증 (Constant Product Formula with Fees)&lt;/h3&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) &amp;gt;= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 Uniswap V2의 핵심인 &lt;b&gt;Constant Product Formula&lt;/b&gt;를 수수료를 포함하여 검증하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Uniswap V2의 수수료:&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;거래당 0.3% (30 basis points)&lt;/li&gt;
&lt;li&gt;이 중 0.05% (5 basis points)는 프로토콜 수수료 (feeTo가 설정된 경우)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 수식으로 표현하면:&lt;br /&gt;$$balance_{adjusted} = balance \times 1000 - amountIn \times 3$$&lt;br /&gt;$$= balance \times 1000 - amountIn \times (1000 \times 0.003)$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 검증 공식:&lt;br /&gt;$$balance0_{adjusted} \times balance1_{adjusted} \geq reserve0 \times reserve1 \times 1000^2$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 풀어쓰면:&lt;br /&gt;$$(balance0 \times 1000 - amount0In \times 3) \times (balance1 \times 1000 - amount1In \times 3) \geq reserve0 \times reserve1 \times 1000^2$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양변을 1000&amp;sup2;로 나누면:&lt;br /&gt;$$\frac{balance0 \times 1000 - amount0In \times 3}{1000} \times \frac{balance1 \times 1000 - amount1In \times 3}{1000} \geq reserve0 \times reserve1$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$(balance0 - \frac{amount0In \times 3}{1000}) \times (balance1 - \frac{amount1In \times 3}{1000}) \geq reserve0 \times reserve1$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$(balance0 - amount0In \times 0.003) \times (balance1 - amount1In \times 0.003) \geq reserve0 \times reserve1$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 수수료(0.3%)를 뺀 후에도 K 값이 유지되거나 증가해야 한다는 의미이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수수료(0.3%)를 제외한 후의 K 값이 이전 K 값보다 크거나 같아야 함&lt;/li&gt;
&lt;li&gt;이는 수수료가 제대로 적용되었는지 확인하는 검증이다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(5) 리저브 업데이트&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증을 통과하면 &lt;code&gt;_update&lt;/code&gt; 함수를 호출하여 리저브를 업데이트하고 가격 누적값도 업데이트한다. 마지막으로 Swap 이벤트를 발생시켜 거래를 기록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Swap 함수의 전체 흐름:&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;입력 검증 (출력량, 유동성 확인)&lt;/li&gt;
&lt;li&gt;Optimistic Transfer (출력 토큰 먼저 전송)&lt;/li&gt;
&lt;li&gt;Flash Swap 콜백 (필요한 경우)&lt;/li&gt;
&lt;li&gt;실제 입력량 계산&lt;/li&gt;
&lt;li&gt;K 값 검증 (수수료 포함)&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;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 사용자가 swap 함수 호출
   └─ amount0Out = 0
   └─ amount1Out = 1000 DAI
   └─ to = MyContract (Flash Swap을 수행할 컨트랙트)
   └─ data = &quot;arbitrage_data&quot; (다른 거래에 필요한 정보)

2. Uniswap Pair가 DAI 전송
   └─ _safeTransfer(DAI, MyContract, 1000)
   └─ Pair 잔액: DAI가 1000 감소

3. uniswapV2Call 콜백 실행
   └─ MyContract.uniswapV2Call() 실행
   └─ 이 함수 내부에서:
      a) 받은 1000 DAI로 다른 거래 수행
      b) 예: 다른 DEX에서 ETH 구매
      c) 그 ETH를 판매하여 DAI + 수익 확보
      d) Uniswap에 지불할 ETH 계산 및 전송

4. balance 확인
   └─ balance0 = IERC20(ETH).balanceOf(Pair)
   └─ balance1 = IERC20(DAI).balanceOf(Pair)
   └─ 만약 MyContract가 ETH를 제대로 전송했다면:
      balance0 = 이전 + 지불할_ETH
      balance1 = 이전 - 1000 (이미 전송됨)

5. 입력량 계산
   └─ amount0In = balance0 - (_reserve0 - 0)
   └─ = 지불한_ETH_양

6. K 값 검증
   └─ 수수료를 포함한 K 값이 유지되는지 확인
   └─ 실패하면 전체 트랜잭션 revert&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;reference&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Uniswap/v2-core&quot;&gt;uniswap v2 core&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://app.uniswap.org/whitepaper.pdf&quot;&gt;uniswap v2 whitepaper&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.soliditylang.org&quot;&gt;solidity 공식문서&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ethervm.io/#F5&quot;&gt;ethervm&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/solidity/what-is-hashing-in-solidity/&quot;&gt;What is Hashing in Solidity?&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발/BlockChain</category>
      <author>TeTedo.</author>
      <guid isPermaLink="true">https://diary-blockchain.tistory.com/395</guid>
      <comments>https://diary-blockchain.tistory.com/395#entry395comment</comments>
      <pubDate>Mon, 3 Nov 2025 22:57:37 +0900</pubDate>
    </item>
    <item>
      <title>10kb 이상 파일 업로드가 안되는데요?</title>
      <link>https://diary-blockchain.tistory.com/394</link>
      <description>&lt;h1&gt;10kb 이상 파일 업로드가 안되는데요?&lt;/h1&gt;
&lt;p&gt;백엔드 서버는 spring boot를 사용하고 있고 파일업로드 기능은 multipart로 파일을 받아 s3에 업로드하는 형식이다.&lt;/p&gt;
&lt;p&gt;파일 업로드가 안되는 이슈를 계속 테스트 해봤고 10kb 이상 파일부터 안올라 가는걸 확인했다.&lt;/p&gt;
&lt;p&gt;나는 어플리케이션의 문제로 인지했고 파일 크기관련 설정을 넣어줬지만 여전히 계속 실패했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 20MB
      max-request-size: 20MB&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;진짜 삽질 너무많이 했는데 결국엔 WAF 문제였다.&lt;/p&gt;
&lt;p&gt;WAF 규칙에 AWSManagedRulesCommonRuleSet 중 SizeRestrictions_BODY 설정이 10kb 이상의 body값은 차단하도록 설정되어있었다.&lt;/p&gt;
&lt;p&gt;이걸 풀어주니 잘 성공했다.&lt;/p&gt;
&lt;h2&gt;교훈&lt;/h2&gt;
&lt;p&gt;미루고 미뤘던 s3 pre signed url 을 적용할때가 온거같다..&lt;/p&gt;</description>
      <category>개발/Devops</category>
      <author>TeTedo.</author>
      <guid isPermaLink="true">https://diary-blockchain.tistory.com/394</guid>
      <comments>https://diary-blockchain.tistory.com/394#entry394comment</comments>
      <pubDate>Sun, 2 Nov 2025 22:33:11 +0900</pubDate>
    </item>
    <item>
      <title>ZK 교육 후기</title>
      <link>https://diary-blockchain.tistory.com/392</link>
      <description>&lt;p&gt;2025.06.28 ~ 2025.08.02 기간에 이더리움 재단에서 주관한 ZK 교육을 듣게된 후기를 작성한다.&lt;/p&gt;
&lt;p&gt;교육은 매주 토요일 판교에서 오프라인으로 진행되었고 매주 리서치한 자료를 발표한걸 듣거나 ZK 관련된 분야의 현직자들의 연사를 들을 수 있었다.&lt;/p&gt;
&lt;h2&gt;1. 계기&lt;/h2&gt;
&lt;p&gt;블록체인쪽으로 커리어를 전향한건 25년 5월부터이다.&lt;/p&gt;
&lt;p&gt;정말 기본적인 것도 모르는게 많았고 블록체인쪽 기술은 너무 많고 새롭게 느껴져서 재밌었다.&lt;/p&gt;
&lt;p&gt;그러던중 지인의 소개로 해당 교육을 알게되었다.&lt;/p&gt;
&lt;p&gt;ZK 라는걸 아예 모르고 있었고 찾아봤는데 영지식증명이라는 이름 자체가 흥미로웠다.&lt;/p&gt;
&lt;p&gt;다른 사람과 함께하면 그만큼 동기부여도 될 것 같았고 다양한 인사이트를 얻을수 있을거 같아 주저없이 신청했다.&lt;/p&gt;
&lt;h2&gt;2. 입문&lt;/h2&gt;
&lt;p&gt;ZK 교육을 신청하고 사전과제로 암호학과 관련된 내용들을 사전습득 해오는게 과제였다.&lt;/p&gt;
&lt;p&gt;대칭키, 비대칭키, 타원곡선, 이산로그 등 블록체인에서 밀접하게 쓰이는 암호학이 주였다.&lt;/p&gt;
&lt;p&gt;아예 모르는 내용들은 아니었지만 자세히 들여다 본적은 없었던 주제들이었고 오랜만에 학문적으로 접근한다는게 흥미로웠다.&lt;/p&gt;
&lt;p&gt;첫날은 사전과제들을 발표하는 자리가 만들어졌고 다른사람들의 수준이 너무 높아 놀랐다.&lt;/p&gt;
&lt;p&gt;ZK에 대한 간단한 개념과 리서치팀, 빌더팀으로 나누어 진행한다고 하여 빌더팀으로 지원했다.&lt;/p&gt;
&lt;h2&gt;3. 벽&lt;/h2&gt;
&lt;p&gt;첫주차를 끝나고 ZK 개념을 참고하라고 몇가지 자료를 받았다.&lt;/p&gt;
&lt;p&gt;PLONK paper: &lt;a href=&quot;https://eprint.iacr.org/2019/953.pdf&quot;&gt;https://eprint.iacr.org/2019/953.pdf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;ZK MOOC: &lt;a href=&quot;https://www.youtube.com/watch?v=A0oZVEXav24&quot;&gt;https://www.youtube.com/watch?v=A0oZVEXav24&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;튜토리얼 블로그&lt;/p&gt;
&lt;p&gt;PLONK by hand: &lt;a href=&quot;https://research.metastate.dev/plonk-by-hand-part-1/&quot;&gt;https://research.metastate.dev/plonk-by-hand-part-1/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;도훈님 블로그: &lt;a href=&quot;https://velog.io/@dohoon8/posts&quot;&gt;https://velog.io/@dohoon8/posts&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;KZG Commitment:&lt;/p&gt;
&lt;p&gt;KZG paper: &lt;a href=&quot;https://www.iacr.org/archive/asiacrypt2010/6477178/6477178.pdf&quot;&gt;https://www.iacr.org/archive/asiacrypt2010/6477178/6477178.pdf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://dankradfeist.de/ethereum/2020/06/16/kate-polynomial-commitments.html&quot;&gt;https://dankradfeist.de/ethereum/2020/06/16/kate-polynomial-commitments.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://scroll.io/blog/kzg&quot;&gt;https://scroll.io/blog/kzg&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.scroll.io/en/learn/zero-knowledge/kzg-commitment-scheme/&quot;&gt;https://docs.scroll.io/en/learn/zero-knowledge/kzg-commitment-scheme/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;참고자료 (KZG, Groth16, FRI, PLONK 등의 강의)&lt;/p&gt;
&lt;p&gt;ZKP MOOC: &lt;a href=&quot;https://rdi.berkeley.edu/zk-learning/&quot;&gt;https://rdi.berkeley.edu/zk-learning/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;제이크 벨로그 : Plonk 의 배경지식에 대하여 &lt;a href=&quot;https://velog.io/@jakeweb3/Plonk-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%82%AC%EC%A0%84-%EC%A7%80%EC%8B%9D&quot;&gt;https://velog.io/@jakeweb3/Plonk-이해하기-사전-지식&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여기서 갑자기 벽이 확 느껴졌다. 들어가보면 모든게 새로운 언어처럼 느껴졌고 머리가 하얘졌다...&lt;/p&gt;
&lt;p&gt;그래서 조금 쉽게 접근할수 있는걸 찾아보다가 디사이퍼에서 교육했던 자료를 찾게 되었고 정말 많은 도움을 얻었다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=8LwaBkFTV88&quot;&gt;디사이퍼 zk 유투브 링크&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;6주라는 짧은 기간에 현업도 하면서 이걸 배울수 있을까 라는 생각이 정말 많이 들었지만 일단 해보기로 했다.&lt;/p&gt;
&lt;h2&gt;4. ZK&lt;/h2&gt;
&lt;p&gt;ZK를 이해하기 위해선 setup, commitment 등 여러가지 개념이 필요했다.&lt;/p&gt;
&lt;p&gt;나름 위 영상을 보면서 조금 이해했지만 종류도 굉장히 많았고 모든걸 이해하긴 시간도 많이 없고 불가능 하다고 느꼈다.&lt;/p&gt;
&lt;p&gt;그래서 리서치 팀에서 발표한 자료들만이라도 이해해보자고 했지만 그것마저도 너무 수학적이라 힘들었다..&lt;/p&gt;
&lt;p&gt;다른 수학과 교수님이 circle stark 에 대해서 발표해주신적이 있는데 굉장히 하이레벨로 설명해주셔서 그나마 이해가 가능했다.&lt;/p&gt;
&lt;p&gt;교육기간동안 다양한 연사분들이 오셨고 여러가지 질문해보고 싶었지만 내가 뭘 모르는것 조차 몰랐기 때문에 질문이 불가능했다..&lt;/p&gt;
&lt;p&gt;그래도 최대한 이해해보려고 몇가지 질문해봤다.&lt;/p&gt;
&lt;h2&gt;5. 언어선택&lt;/h2&gt;
&lt;p&gt;교육의 진행은 리서치팀과 빌더팀으로 완전 다르게 진행되었다.&lt;/p&gt;
&lt;p&gt;빌더팀은 하나의 애플리케이션을 목표로 했고 리서치 팀은 좀 더 세부적으로 zk 를 탐구하는것이었다.&lt;/p&gt;
&lt;p&gt;언어는 3가지로 cairo, noir, halo2 중에 선택을 할 수 있었는데 우선 튜토리얼을 해보고 결정하기로 했다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/TeTedo/blog-code/tree/main/zk-start-cairo&quot;&gt;cairo 튜토리얼&lt;/a&gt; 과 &lt;a href=&quot;https://github.com/TeTedo/blog-code/tree/main/zk-start-noir&quot;&gt;noir 튜토리얼&lt;/a&gt;을 해봤는데 noir는 sdk 로 정말 잘 되어있어서 오히려 cairo 쪽을 더 해보고 싶었다. 근데 결국엔 noir 쪽으로 선택했다.&lt;/p&gt;
&lt;h2&gt;6. 어플리케이션&lt;/h2&gt;
&lt;p&gt;어떤 어플리케이션을 만들어볼까 고민을 많이 했다.&lt;/p&gt;
&lt;p&gt;ZK를 사용하면 꽤 많은걸 할수 있다고 생각했지만 생각보다 한정적이었다.&lt;/p&gt;
&lt;p&gt;여러 아이디어 중에 일반인들에게 ZK로 가장 쉽게 접할수 있을게 뭐가 있을까 하다가 zk-survey 라는 설문 플랫폼을 만들기로 했다.&lt;/p&gt;
&lt;p&gt;팀원이 블록체인쪽 기술을 여러가지 많이 알고 있어서 도움을 많이 받았다.&lt;/p&gt;
&lt;h2&gt;7. zk-survey &lt;a href=&quot;https://github.com/ZK-Proofer/zk-survey&quot;&gt;깃허브 링크&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;이제 어떤걸 zk 로 증명하냐를 고민하다가 설문내용을 zkp 로 증명하여 권한이 있는사람이 설문을 제출했다 라는걸 증명하기로 했다.&lt;/p&gt;
&lt;p&gt;그러기 위해선 먼저 권한을 부여하는 작업이 필요했다.&lt;/p&gt;
&lt;p&gt;그래서 플로우를 다음과 같이 짰다.&lt;/p&gt;
&lt;p&gt;설문 생성 -&amp;gt; 설문자 등록 -&amp;gt; 설문 제출&lt;/p&gt;
&lt;p&gt;설문 제출을 할때 권한을 가지고 있는지 proof를 만들어 증명하는것으로 했고 멤버쉽 프루프를 이용했다.&lt;/p&gt;
&lt;p&gt;일반인이 쉽게 zk를 접해보는것이 목적이었으므로 온체인 데이터는 사용하지 않았다.&lt;/p&gt;
&lt;p&gt;그래서 sdk 가 잘 되어있는 noir 로 써킷과 prover 를 구성했다.&lt;/p&gt;
&lt;h2&gt;8. 후기&lt;/h2&gt;
&lt;p&gt;살면서 가장 머리아팠던 6주라고 꼽을수 있다.&lt;/p&gt;
&lt;p&gt;새로운 지식, 어려운 수식, 난잡한 공식 등 머리론 이해 안되지만 나름 이해해보려는 싸움을 계속 했다.&lt;/p&gt;
&lt;p&gt;좋았던건 누군가와 이해하는 과정을 공유하며 배울수 있었고 인사이트도 많이 얻었다.&lt;/p&gt;
&lt;p&gt;zk를 짧은시간에 집어넣으려다보니 버거웠지만 교육이 끝난 후 여기저기서 zk 관련된 글자가 보이면 나도 모르게 관심이 간다.&lt;/p&gt;
&lt;p&gt;누군가에게 기술적으로 설명하는건 못하지만 다른사람이 썼을때 어떻게 썼는지 질문할 정도는 된다고 생각한다.&lt;/p&gt;
&lt;p&gt;쉽지 않은 주제였던만큼 나름 보람도 있고 어플리케이션도 완성은 해서 값진 6주라고 생각한다.&lt;/p&gt;</description>
      <category>개발/BlockChain</category>
      <author>TeTedo.</author>
      <guid isPermaLink="true">https://diary-blockchain.tistory.com/392</guid>
      <comments>https://diary-blockchain.tistory.com/392#entry392comment</comments>
      <pubDate>Mon, 6 Oct 2025 19:30:23 +0900</pubDate>
    </item>
    <item>
      <title>[AWS ECS] 설정하면서 이슈들</title>
      <link>https://diary-blockchain.tistory.com/391</link>
      <description>&lt;p&gt;지금까지 ec2 인스턴스 하나만 배포하여 사용하고 있었다.&lt;br&gt;생각보다 꽤나 잘버티고 있지만 저번에 동시에 엄청난 요청을 받아들이면서 서버가 매우 느려진 시기가 있었다.&lt;br&gt;지금은 그정도는 아니지만 나름 여유가 있을때 ecs 로 바꾸면서 오토스케일링 및 롤링업데이트도 적용하려고 한다.&lt;/p&gt;
&lt;p&gt;기존에는 ecr 에 이미지를 올리고 ec2 에서 그 이미지를 pull 땡겨와서 배포를 진행했다.&lt;br&gt;굳이 eks 까지 쓸이유는 없다고 판단해서 기존 배포를 ecs 배포로 마이그레이션 하려고 한다.&lt;/p&gt;
&lt;h2&gt;1. EC2 vs Fargate&lt;/h2&gt;
&lt;p&gt;제일 먼저 맞닥뜨린 고민은 ecs에서 launch type인 ec2와 fargate의 선택이다.&lt;/p&gt;
&lt;p&gt;나는 ecs뿐만 아니라 서버리스의 환경을 운영해본적은 없고 ec2에 훨씬 익숙하다.&lt;br&gt;예전이라면 주저없이 ec2를 사용했을것이다. 더 싸고 익숙하니까.&lt;br&gt;하지만 지금은 인프라에 많은 리소스를 쏟아붓고 싶지 않고 좀 더 비즈니스 쪽으로 집중하고 싶었다.&lt;/p&gt;
&lt;p&gt;그리고 확장성을 고려한다면 ec2보다 fargate가 유리하다고 생각했다.&lt;br&gt;현재는 하나의 서버를 돌리지만 부하가 되는 몇몇 도메인을 분리해야겠다는 생각이 있기 때문이다.&lt;br&gt;msa 까진 아니더라도 여러 도메인의 서버가 늘어날수 있는 상황이기 때문이다.&lt;/p&gt;
&lt;p&gt;이것저것 찾아보다가 fargate로 굳히게된 글이 있었다.&lt;br&gt;&amp;quot;ecs 비용이 백만원 넘어가면 이미 회사는 돈이 넘쳐날것이다.&amp;quot;&lt;br&gt;이 글을 보고 고민이 확신으로 바뀌게 되었다.&lt;/p&gt;
&lt;p&gt;비용은 Compute Savings Plan, AWS fargate spot 등을 이용하여 최대한 효율적으로 내보려고 한다.&lt;/p&gt;
&lt;p&gt;추가로 최근에 fargate 에서 ec2 인스턴스를 사용할수 있도록 서비스를 내놓았다고 하는데 서울지역을 지원하지 않기 때문에 패스했다. 서울지역까지 추가된다면 충분히 고려할만한 조건이다. 비용적으로도 관리적으로도 ec2가 훨씬 나에게 익숙하기 때문이다.&lt;/p&gt;
&lt;h2&gt;2. ecs 세팅&lt;/h2&gt;
&lt;p&gt;아래는 미래의 까먹을 나를 위해 정리한 순서.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ksh-cloud.tistory.com/52&quot;&gt;여기&lt;/a&gt; 참고해서 구성했다.&lt;/p&gt;
&lt;h3&gt;(1) 클러스터 생성&lt;/h3&gt;
&lt;p&gt;fargate생성하고 클러스터 생성했다.&lt;/p&gt;
&lt;h3&gt;(2) ALB 구성&lt;/h3&gt;
&lt;p&gt;target group 과 alb 를 설정해준다.&lt;br&gt;타겟그룹에선 ip로 만들고 ec2 안잡아줘도 된다.&lt;/p&gt;
&lt;p&gt;alb 에서 http:80 포트를 target group 으로 설정&lt;/p&gt;
&lt;h3&gt;(3) 태스크 정의&lt;/h3&gt;
&lt;p&gt;새 태스크 생성&lt;br&gt;태스크 역할에 ecsTaskExecutionRole 부여&lt;/p&gt;
&lt;h3&gt;(4) 서비스 구성&lt;/h3&gt;
&lt;p&gt;컴퓨팅 옵션 - 시작 유형으로 선택&lt;br&gt;배포옵션 - 디폴트가 롤링 업데이트로 되어있음&lt;/p&gt;
&lt;h2&gt;3. 구성하는데 이슈&lt;/h2&gt;
&lt;p&gt;(1) 환경변수 이슈&lt;br&gt;기존 환경변수는 key value 값이 아닌 yaml 파일형식으로 관리하고 있었음.&lt;br&gt;하지만 ecs 에서 제공하는 형식에 따르기 위해 key value 형식으로 바꿔서 주입해주는것으로 결정하고 바꿈&lt;/p&gt;
&lt;p&gt;(2) inbound 이슈&lt;br&gt;첫번째는 rds 와의 연결 이슈가 있었다.&lt;br&gt;ecs service 에서 rds 와 연결하는 과정에서 rds 의 security group 이슈가 있음.&lt;br&gt;ecs 에서 사용하고 있는 security group 을 인바운드로 허용해줌으로 트래픽 허용&lt;/p&gt;
&lt;p&gt;두번째는 alb 와 ecs 사이에서의 인바운드 이슈이다.&lt;br&gt;alb 와 ecs 서비스에서 각각 다른 security group 을 사용했는데&lt;br&gt;ecs 에서 alb 의 트래픽을 허용하고 있지 않았다.&lt;br&gt;이것도 마찬가지로 alb 의 security group 을 ecs 에서 허용해주는거로 설정했다.&lt;/p&gt;
&lt;p&gt;맨날 ip 로만 inbound를 설정해오다가 security group 자체를 인바운드 설정으로 추가할 수 있다는 사실을 처음 알게 되었다. 앞으로도 많이 써먹을것 같다. 그만큼 security 관리도 해야하겠지만..&lt;/p&gt;</description>
      <category>개발/AWS</category>
      <author>TeTedo.</author>
      <guid isPermaLink="true">https://diary-blockchain.tistory.com/391</guid>
      <comments>https://diary-blockchain.tistory.com/391#entry391comment</comments>
      <pubDate>Mon, 6 Oct 2025 13:52:35 +0900</pubDate>
    </item>
    <item>
      <title>[Blockchain] kaiascan은 token transfer 를 어떻게 처리할까</title>
      <link>https://diary-blockchain.tistory.com/390</link>
      <description>&lt;h1&gt;kaiascan은 token transfer 를 어떻게 처리할까&lt;/h1&gt;
&lt;p&gt;예시로 만든 컨트랙트는 &lt;a href=&quot;https://github.com/TeTedo/blog-code/tree/main/kaia-token-transfer&quot;&gt;깃허브&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;p&gt;메인넷 익스플로러에서 token에 대한 전송과정은 db에 저장해놓고 보여줘야 한다.&lt;/p&gt;
&lt;p&gt;하지만 트랜잭션만 보고 토큰을 전송한건지 뭐한건지는 정확히 알수 없다. 추측은 가능할뿐..&lt;/p&gt;
&lt;p&gt;카이아 코인도 몇개 있고 해서 카이아에서 테스트해보기로 결정했다.&lt;/p&gt;
&lt;p&gt;그래서 카이아에서 토큰 트랜스퍼를 어떻게 감지하는가에 대한 테스트를 해보고 뇌피셜로 정리해보려고 한다.&lt;/p&gt;
&lt;p&gt;여러가지 컨트랙트를 erc20에 기반하여 배포해보고 실제로 전송해보며 테스트를 진행했다.&lt;/p&gt;
&lt;h2&gt;1. Transfer event 다 제거해보고 배포&lt;/h2&gt;
&lt;p&gt;token 으로 아예 인식 안함&lt;/p&gt;
&lt;p&gt;토큰전송을 해봐도 token transfer 에 기록이 안남는다.&lt;br&gt;물론 balance 도 안찍힘&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kaiascan.io/address/0xa1e58444fa16cd98dad75e0dda8408f27a795090?tabId=txList&amp;amp;page=1&quot;&gt;https://kaiascan.io/address/0xa1e58444fa16cd98dad75e0dda8408f27a795090?tabId=txList&amp;amp;page=1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;eip 20 에 대한 규격 참고링크&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://eips.ethereum.org/EIPS/eip-20&quot;&gt;https://eips.ethereum.org/EIPS/eip-20&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;eip20 에서는 transfer 이벤트가 필수다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 receipt 에서 구분할 수 있는 Transfer 이벤트를 활용하면 토큰인지 구분할수 있다.&lt;/p&gt;
&lt;h2&gt;2. Token Transfer Event 개떡같이 설정하고 배포&lt;/h2&gt;
&lt;p&gt;실제 토큰을 전송한 value * 2 로 이벤트 생성&lt;/p&gt;
&lt;p&gt;결과&lt;/p&gt;
&lt;p&gt;token transfer 는 event 에 발생한 양으로 된다.&lt;br&gt;근데 token balance 는 진짜 balance 로 찍힌다.&lt;/p&gt;
&lt;p&gt;-&amp;gt; token transfer 와 balance 의 연관관계는 없는듯 하다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kaiascan.io/token/0x900efd7a984ad25312866a3125e8e52965b5cfa2?tabId=tokenTransfer&amp;amp;page=1&quot;&gt;https://kaiascan.io/token/0x900efd7a984ad25312866a3125e8e52965b5cfa2?tabId=tokenTransfer&amp;amp;page=1&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;3. Fake Token Transfer&lt;/h2&gt;
&lt;p&gt;tranfer 이벤트만 발생시키고 실제론 balance 안바꿔주기&lt;/p&gt;
&lt;p&gt;token transfer 는 나오지만 balance는 0으로 찍힌다.&lt;/p&gt;
&lt;p&gt;-&amp;gt; 2번과 마찬가지로 둘사이의 연관성은 없다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kaiascan.io/token/0x3a2f0d3195b61b7cb2556de2486d5692a700a4ba?tabId=tokenTransfer&amp;amp;page=1&quot;&gt;https://kaiascan.io/token/0x3a2f0d3195b61b7cb2556de2486d5692a700a4ba?tabId=tokenTransfer&amp;amp;page=1&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;4. 그럼 transfer 이벤트를 배포할때 감지가능??&lt;/h2&gt;
&lt;p&gt;배포할때 생기는 바이트코드에서 transfer 이벤트가 있는지 감지가능한가?&lt;/p&gt;
&lt;p&gt;transfer 이벤트를 keccak 으로 해시후 바이트코드에 포함되어있는지 확인??&lt;/p&gt;
&lt;p&gt;가능하다.&lt;/p&gt;
&lt;p&gt;그럼 erc20에서 필수로 만들어야하는 메소드, 이벤트들은 컨트랙트가 배포될때 확인해볼수 있다.&lt;/p&gt;
&lt;h2&gt;5. ERC20 규격은 안따르지만 토큰처럼 생겼다면?&lt;/h2&gt;
&lt;p&gt;Transfer(address,address,uint256) 이벤트를 발생시키면서 totalSupply, balanceOf 함수가 있다면 토큰으로 간주한다.&lt;/p&gt;
&lt;p&gt;대표적인 예시가 WKaia 이다.&lt;/p&gt;
&lt;p&gt;Dex 에서 Wrapped coin 은 erc20 규격에 따르진 않지만 totalSupply 와 BalanceOf 를 가지고있다.&lt;/p&gt;
&lt;p&gt;kaia -&amp;gt; Wkaia 로 바꿀때는 Transfer 이벤트가 발생되지 않는다. deposit 이벤트만 발생한다.&lt;/p&gt;
&lt;p&gt;실제론 Wkaia의 balance가 올라갔지만 Transfer 이벤트는 발생하지 않기 때문에 erc20 규격은 만족하지 않는다.&lt;/p&gt;
&lt;p&gt;하지만 Wkaia 로 다른 토큰과 스왑을 한다면 그때는 Transfer 이벤트를 발생시킨다.&lt;/p&gt;
&lt;p&gt;이때 토큰으로 감지해서 Balance도 생긴다.&lt;/p&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Transfer(address,address,uint256) 이벤트를 발생시키면서 totalSupply, balanceOf 함수가 있다면 토큰으로 간주한다.&lt;/li&gt;
&lt;li&gt;token transfer 는 이벤트에 의존한다. (이벤트의 값을 그대로 저장)&lt;/li&gt;
&lt;li&gt;token balance는 실제 컨트랙트에서 balanceOf 를 호출해온다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/BlockChain</category>
      <author>TeTedo.</author>
      <guid isPermaLink="true">https://diary-blockchain.tistory.com/390</guid>
      <comments>https://diary-blockchain.tistory.com/390#entry390comment</comments>
      <pubDate>Tue, 22 Jul 2025 10:50:35 +0900</pubDate>
    </item>
    <item>
      <title>[CS] TCP vs UDP의 차이점과 사용 사례</title>
      <link>https://diary-blockchain.tistory.com/389</link>
      <description>&lt;h1&gt;TCP vs UDP의 차이점과 사용 사례&lt;/h1&gt;
&lt;h2&gt;목차&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9A%94&quot;&gt;개요&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#tcp-transmission-control-protocol&quot;&gt;TCP (Transmission Control Protocol)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#udp-user-datagram-protocol&quot;&gt;UDP (User Datagram Protocol)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#tcp-vs-udp-%EB%B9%84%EA%B5%90&quot;&gt;TCP vs UDP 비교&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%9C-%EC%82%AC%EC%9A%A9-%EC%82%AC%EB%A1%80&quot;&gt;실제 사용 사례&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%A0%95%EB%A6%AC&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;네트워크 통신에서 데이터를 전송하는 방법은 크게 두 가지로 나뉩니다: &lt;strong&gt;TCP(Transmission Control Protocol)&lt;/strong&gt; 와 &lt;strong&gt;UDP(User Datagram Protocol)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;이 두 프로토콜은 각각 다른 특성을 가지고 있어서, 용도에 따라 적절한 프로토콜을 선택하는 것이 중요합니다.&lt;/p&gt;
&lt;h2&gt;TCP (Transmission Control Protocol)&lt;/h2&gt;
&lt;h3&gt;특징&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;연결 지향적 (Connection-oriented)&lt;/strong&gt;: 통신 전에 연결을 먼저 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신뢰성 보장&lt;/strong&gt;: 데이터 손실, 중복, 순서 보장&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;흐름 제어&lt;/strong&gt;: 수신 측의 처리 능력에 맞춰 데이터 전송 속도 조절&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;혼잡 제어&lt;/strong&gt;: 네트워크 상황에 따라 전송 속도 조절&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;동작 과정&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 3-way handshake (연결 설정)
   Client → SYN → Server
   Client ← SYN + ACK ← Server
   Client → ACK → Server

2. 데이터 전송
   Client ↔ 데이터 ↔ Server

3. 4-way handshake (연결 해제)
   Client → FIN → Server
   Client ← ACK ← Server
   Client ← FIN ← Server
   Client → ACK → Server&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;데이터 무결성 보장&lt;/li&gt;
&lt;li&gt;순서 보장&lt;/li&gt;
&lt;li&gt;자동 재전송&lt;/li&gt;
&lt;li&gt;흐름 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;오버헤드가 큼&lt;/li&gt;
&lt;li&gt;속도가 상대적으로 느림&lt;/li&gt;
&lt;li&gt;실시간성이 떨어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;UDP (User Datagram Protocol)&lt;/h2&gt;
&lt;h3&gt;특징&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;비연결형 (Connectionless)&lt;/strong&gt;: 연결 설정 없이 바로 데이터 전송&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신뢰성 없음&lt;/strong&gt;: 데이터 손실 가능성 있음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;순서 보장 없음&lt;/strong&gt;: 패킷 순서가 바뀔 수 있음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;빠른 전송&lt;/strong&gt;: 오버헤드가 적어 빠름&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;동작 과정&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 연결 설정 없음
2. 데이터 전송
   Client → 데이터 → Server
   (재전송, 순서 보장 없음)
3. 연결 해제 없음&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;빠른 전송 속도&lt;/li&gt;
&lt;li&gt;실시간성 보장&lt;/li&gt;
&lt;li&gt;오버헤드가 적음&lt;/li&gt;
&lt;li&gt;단순한 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;데이터 손실 가능성&lt;/li&gt;
&lt;li&gt;순서 보장 없음&lt;/li&gt;
&lt;li&gt;신뢰성 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;TCP vs UDP 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;TCP&lt;/th&gt;
&lt;th&gt;UDP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;연결 방식&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;연결 지향적&lt;/td&gt;
&lt;td&gt;비연결형&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;신뢰성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;높음 (재전송, 순서 보장)&lt;/td&gt;
&lt;td&gt;낮음 (손실 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;속도&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;상대적으로 느림&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;오버헤드&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;실시간성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;사용 포트&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1-65535&lt;/td&gt;
&lt;td&gt;1-65535&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;헤더 크기&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20-60 bytes&lt;/td&gt;
&lt;td&gt;8 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;실제 사용 사례&lt;/h2&gt;
&lt;h3&gt;TCP 사용 사례&lt;/h3&gt;
&lt;h4&gt;1. 웹 브라우징 (HTTP/HTTPS)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 웹사이트 접속 시 TCP 사용
curl -v https://www.google.com
# TCP 3-way handshake 후 데이터 전송&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 이메일 (SMTP, POP3, IMAP)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 이메일 서버와의 통신
telnet smtp.gmail.com 587&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 파일 전송 (FTP)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 파일 업로드/다운로드
ftp ftp.example.com&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. SSH (원격 접속)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 서버 원격 접속
ssh user@server.com&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;UDP 사용 사례&lt;/h3&gt;
&lt;h4&gt;1. 실시간 스트리밍&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 예시: 실시간 비디오 스트리밍
import socket

# UDP 소켓 생성
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(video_frame, (&amp;#39;192.168.1.100&amp;#39;, 5000))&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 온라인 게임&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 게임 패킷 전송 예시
game_packet = {
    &amp;#39;player_id&amp;#39;: 123,
    &amp;#39;position&amp;#39;: {&amp;#39;x&amp;#39;: 100, &amp;#39;y&amp;#39;: 200},
    &amp;#39;action&amp;#39;: &amp;#39;move&amp;#39;
}
sock.sendto(json.dumps(game_packet).encode(), (server_ip, game_port))&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. DNS 조회&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# DNS 쿼리 (UDP 사용)
nslookup google.com&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. DHCP (IP 주소 할당)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# DHCP 서버로부터 IP 주소 요청
dhclient eth0&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. VoIP (음성 통화)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 음성 데이터 전송
audio_data = capture_audio()
sock.sendto(audio_data, (remote_ip, voice_port))&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;h3&gt;TCP&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;데이터 무결성이 중요한 경우&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파일 전송&lt;/li&gt;
&lt;li&gt;이메일 전송&lt;/li&gt;
&lt;li&gt;웹 페이지 로딩&lt;/li&gt;
&lt;li&gt;데이터베이스 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;순서가 중요한 경우&lt;/strong&gt;&lt;/p&gt;
&lt;ul&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;h3&gt;UDP&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;실시간성이 중요한 경우&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;실시간 비디오 스트리밍&lt;/li&gt;
&lt;li&gt;온라인 게임&lt;/li&gt;
&lt;li&gt;VoIP 통화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;빠른 전송이 필요한 경우&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DNS 조회&lt;/li&gt;
&lt;li&gt;DHCP&lt;/li&gt;
&lt;li&gt;실시간 모니터링&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;일부 데이터 손실이 허용되는 경우&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;실시간 센서 데이터&lt;/li&gt;
&lt;li&gt;로그 데이터 (실시간성 우선)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TCP와 UDP는 각각의 장단점이 있어서, &lt;strong&gt;용도에 맞는 프로토콜을 선택하는 것이 중요&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TCP&lt;/strong&gt;: 신뢰성이 중요한 웹, 이메일, 파일 전송 등&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UDP&lt;/strong&gt;: 실시간성이 중요한 스트리밍, 게임, VoIP 등&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/CS</category>
      <author>TeTedo.</author>
      <guid isPermaLink="true">https://diary-blockchain.tistory.com/389</guid>
      <comments>https://diary-blockchain.tistory.com/389#entry389comment</comments>
      <pubDate>Tue, 22 Jul 2025 10:41:40 +0900</pubDate>
    </item>
    <item>
      <title>[Blockchain] ZK - noir, cairo 섞어쓰기</title>
      <link>https://diary-blockchain.tistory.com/388</link>
      <description>&lt;h1&gt;ZK - noir, cairo 섞어쓰기&lt;/h1&gt;
&lt;p&gt;모든 코드는 &lt;a href=&quot;https://github.com/TeTedo/blog-code/tree/main/zk-noir-cairo&quot;&gt;깃허브&lt;/a&gt;에서 볼수 있습니다.&lt;/p&gt;
&lt;p&gt;이전에 &lt;a href=&quot;https://github.com/TeTedo/blog-code/tree/main/zk-cairo-age-verify&quot;&gt;ZK [Cairo] - 나이 인증 회로 구현&lt;/a&gt; 에서 나이 인증 회로를 만들어봤다.&lt;/p&gt;
&lt;p&gt;위 과정에선 굉장한 문제점이 있다.&lt;/p&gt;
&lt;p&gt;zk proof 를 만들때 포세이돈 해시로 만드는데 이 과정만 알아버리면 누구든지 verify를 통과하는 proof를 만들수 있다는 것이다.&lt;/p&gt;
&lt;p&gt;그래서 이 부분을 숨기고싶었다.&lt;/p&gt;
&lt;p&gt;굉장히 많이 찾아봤지만 cairo 에서 proof를 만들고 온체인에서 verify를 하는걸 못 찾았다.&lt;/p&gt;
&lt;p&gt;그래서 noir로 circuit을 짜고 proof를 만들어서 noir가 제공해주는 verifier를 통해서 구현해보고 싶었다.&lt;/p&gt;
&lt;h2&gt;(1) Noir circuit&lt;/h2&gt;
&lt;p&gt;다시 보니 vscode 에는 noir 하이라이팅 익스텐션이 있는데 cursor 에는 없다.&lt;/p&gt;
&lt;p&gt;예전에 했던 &lt;a href=&quot;https://github.com/TeTedo/blog-code/tree/main/zk-start-noir&quot;&gt;ZK - Noir 시작해보기&lt;/a&gt;를 참고해서 기억을 더듬어 해보기로 했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p circuit/src
touch circuit/src/main.nr circuit/Nargo.toml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;main.nr&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nr&quot;&gt;fn main(age: u32, nonce: u32, min_age: pub u32) {
  assert(age &amp;gt;= min_age);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nargo.toml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[package]
name = &amp;quot;circuit&amp;quot;
type = &amp;quot;bin&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;circuit 폴더 들어가서 compile&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd circuit
nargo compile&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;(2) verifier 만들기&lt;/h2&gt;
&lt;p&gt;먼저 &lt;a href=&quot;https://garaga.gitbook.io/garaga/installation&quot;&gt;developer 환경설정&lt;/a&gt; 에서 필요한 이것저것 설치해준다. python scarb 등등&lt;/p&gt;
&lt;p&gt;그다음 bbup라는 cli를 설치해야 한다.&lt;/p&gt;
&lt;p&gt;Barretenberg은 Aztec을 위한 ZK prover backend 라고 하는데 이걸위한 cli 라고한다. 나도 잘 모름&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/master/barretenberg/bbup/install | bash&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;garaga 설치&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip3 install garaga==0.18.1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;bb 설치&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bbup --version 0.87.4-starknet.1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;먼저 verify key 를 만든다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bb write_vk --scheme ultra_honk --oracle_hash keccak -b target/circuit.json -o target&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;verifier 생성&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;garaga gen --system ultra_starknet_zk_honk --vk target/vk&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;프로젝트 이름을 넣으면 cairo로 된 코드가 자동 생성된다.&lt;/p&gt;
&lt;h2&gt;(3) verifier 배포&lt;/h2&gt;
&lt;h3&gt;Local 배포&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;starknet-devnet --seed=0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;verifier 경로로 이동 후 sfoundry.toml 에다가 계정정보 넣어두고&lt;/p&gt;
&lt;p&gt;declare&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sncast --profile=devnet declare \
    --contract-name=UltraStarknetZKHonkVerifier&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;result&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;command: declare
class_hash: 0x056792130b48aef444ce7c422421fec1299e5be2d4bc5460e62b8fe6c53dbced
transaction_hash: 0x03ed22efba77549ed3fb941fd511e36c5c62036f95c7d07dbafa5b556d56b3c2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;deploy&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sncast --profile=devnet deploy \
    --class-hash=0x056792130b48aef444ce7c422421fec1299e5be2d4bc5460e62b8fe6c53dbced \
    --salt=0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;result&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;command: deploy
contract_address: 0x008e314294aa3362b8d8add557e95e93b28513c4f904a87cab4c25497f843576
transaction_hash: 0x01aaa33c8001d94971cf8908ec0e5566b0cf0a374ef2cb7d9fa84dad45f6ff27&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;(4) proof 만들기&lt;/h2&gt;
&lt;p&gt;circuit 폴더로 돌아가서 Prover.toml 파일을 만들어서 witness 를 만들고 witness로 proof를 만들어줄것이다.&lt;/p&gt;
&lt;p&gt;Prover.toml&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;age = 25
min_age = 20&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;witness 생성&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nargo execute witness&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;proof 생성&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bb prove -s ultra_honk --oracle_hash starknet --zk -b target/circuit.json -w target/witness.gz -o target/&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;(5) proof 검증&lt;/h2&gt;
&lt;p&gt;cli 검증&lt;/p&gt;
&lt;p&gt;call data 생성&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;garaga calldata --system ultra_starknet_zk_honk --proof target/proof --vk target/vk --public-inputs target/public_inputs &amp;gt; ../verifier/calldata.text&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;매우긴 call data 가 생성된다.&lt;/p&gt;
&lt;p&gt;contract 검증&lt;/p&gt;
&lt;p&gt;verifier 쪽으로 폴더 이동후 실행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sncast --profile=devnet call \
    --contract-address=0x008e314294aa3362b8d8add557e95e93b28513c4f904a87cab4c25497f843576 \
    --function=verify_ultra_starknet_zk_honk_proof \
    --calldata $(cat calldata.txt)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;result&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;command: call
response: [0x0, 0x1, 0x14, 0x0]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;contract 에 있는 주석을 보면 성공했을때 public input 을 return 한다고 한다. 실패시 반환 안한다고함.&lt;/p&gt;
&lt;p&gt;3번째 인자로 public input인 20이 나왔다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// This function returns an Option for the public inputs if the proof is valid.
// If the proof is invalid, the execution will either fail or return None.
0x0 (0): 첫 번째 public input (시작 인덱스)
0x1 (1): 성공 플래그 또는 상태 값
0x14 (20): min_age = 20 - 우리가 설정한 최소 나이
0x0 (0): 마지막 public input (종료 인덱스)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;번외로 circuit 에 만족하지 않으면 witness 가 생성이 안된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;error: Failed constraint
  ┌─ src/main.nr:2:10
  │
2 │   assert(age &amp;gt;= min_age);
  │          --------------
  │
  = Call stack:
    1. src/main.nr:2:10

Failed to solve program: &amp;#39;Cannot satisfy constraint&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;레퍼런스&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://garaga.gitbook.io/garaga/smart-contract-generators/noir&quot;&gt;Noir Verifier&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/AztecProtocol/aztec-packages/blob/master/barretenberg/bbup/README.md&quot;&gt;BBup README&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/BlockChain</category>
      <author>TeTedo.</author>
      <guid isPermaLink="true">https://diary-blockchain.tistory.com/388</guid>
      <comments>https://diary-blockchain.tistory.com/388#entry388comment</comments>
      <pubDate>Sun, 13 Jul 2025 22:32:50 +0900</pubDate>
    </item>
    <item>
      <title>[DB] MySQL 인덱스 사용 가이드</title>
      <link>https://diary-blockchain.tistory.com/387</link>
      <description>&lt;h1&gt;MySQL 인덱스 (Index) 사용 가이드&lt;/h1&gt;
&lt;h2&gt;목차&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%9D%B8%EB%8D%B1%EC%8A%A4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80&quot;&gt;인덱스란 무엇인가?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%9D%B8%EB%8D%B1%EC%8A%A4%EC%9D%98-%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC&quot;&gt;인덱스의 작동 원리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%9D%B8%EB%8D%B1%EC%8A%A4%EC%9D%98-%EC%A2%85%EB%A5%98&quot;&gt;인덱스의 종류&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EA%B4%80%EB%A6%AC&quot;&gt;인덱스 생성 및 관리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94&quot;&gt;인덱스 성능 최적화&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%9C-%EC%98%88%EC%A0%9C&quot;&gt;실제 예제&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;인덱스란 무엇인가?&lt;/h2&gt;
&lt;p&gt;인덱스는 데이터베이스에서 데이터를 빠르게 찾을 수 있도록 도와주는 자료구조입니다. 책의 목차나 색인과 같은 역할을 하며, 전체 테이블을 스캔하지 않고도 원하는 데이터를 빠르게 찾을 수 있게 해줍니다.&lt;/p&gt;
&lt;h3&gt;인덱스가 필요한 이유&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;전체 테이블 스캔(Full Table Scan) 방지&lt;/strong&gt;: 대용량 데이터에서 성능 향상&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정렬된 데이터 접근&lt;/strong&gt;: ORDER BY, GROUP BY 성능 개선&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;중복 값 제거&lt;/strong&gt;: DISTINCT 연산 최적화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;조인 성능 향상&lt;/strong&gt;: 외래키 인덱스 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;인덱스의 작동 원리&lt;/h2&gt;
&lt;h3&gt;1. B-Tree 구조&lt;/h3&gt;
&lt;p&gt;MySQL의 기본 인덱스는 B-Tree(Balanced Tree) 구조를 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        [Root Node]
        /     \
   [Leaf]     [Leaf]
   /   \        /   \
[Data] [Data] [Data] [Data]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;B-Tree의 특징:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모든 리프 노드가 같은 레벨에 위치&lt;/li&gt;
&lt;li&gt;각 노드는 여러 개의 키를 가질 수 있음&lt;/li&gt;
&lt;li&gt;검색, 삽입, 삭제가 모두 O(log n) 시간복잡도&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 인덱스 검색 과정&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 예시: users 테이블에서 name이 &amp;#39;John&amp;#39;인 사용자 검색
SELECT * FROM users WHERE name = &amp;#39;John&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;인덱스가 없는 경우:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;전체 테이블 스캔 (Full Table Scan)&lt;/li&gt;
&lt;li&gt;모든 행을 순차적으로 검사&lt;/li&gt;
&lt;li&gt;시간복잡도: O(n)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;인덱스가 있는 경우:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;인덱스 트리에서 &amp;#39;John&amp;#39; 검색&lt;/li&gt;
&lt;li&gt;해당 키의 포인터로 실제 데이터 위치 확인&lt;/li&gt;
&lt;li&gt;시간복잡도: O(log n)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3. 인덱스의 물리적 구조&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[인덱스 페이지]
┌─────────────────┐
│ Key │ Pointer   │
├─────────────────┤
│ A   │ Page 1    │
│ B   │ Page 2    │
│ C   │ Page 3    │
└─────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;인덱스의 종류&lt;/h2&gt;
&lt;h3&gt;1. 클러스터형 인덱스 (Clustered Index)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 데이터가 물리적으로 정렬되어 저장&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;제한&lt;/strong&gt;: 테이블당 하나만 생성 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;기본값&lt;/strong&gt;: PRIMARY KEY가 클러스터형 인덱스&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 클러스터형 인덱스 예시
CREATE TABLE users (
    id INT PRIMARY KEY,  -- 클러스터형 인덱스
    name VARCHAR(100),
    email VARCHAR(100)
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 비클러스터형 인덱스 (Non-Clustered Index)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 별도의 인덱스 구조에 포인터 저장&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;제한&lt;/strong&gt;: 테이블당 여러 개 생성 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;용도&lt;/strong&gt;: 보조 인덱스, 복합 인덱스&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 비클러스터형 인덱스 예시
CREATE INDEX idx_name ON users(name);
CREATE INDEX idx_email ON users(email);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 복합 인덱스 (Composite Index)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 여러 컬럼을 조합한 인덱스&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;순서&lt;/strong&gt;: 컬럼 순서가 중요 (왼쪽 우선)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 복합 인덱스 예시
CREATE INDEX idx_name_email ON users(name, email);

-- 효율적인 쿼리
SELECT * FROM users WHERE name = &amp;#39;John&amp;#39; AND email = &amp;#39;john@example.com&amp;#39;;
SELECT * FROM users WHERE name = &amp;#39;John&amp;#39;;  -- 인덱스 사용 가능

-- 비효율적인 쿼리
SELECT * FROM users WHERE email = &amp;#39;john@example.com&amp;#39;;  -- 인덱스 사용 불가&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 고유 인덱스 (Unique Index)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 중복 값을 허용하지 않음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;용도&lt;/strong&gt;: 데이터 무결성 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 고유 인덱스 예시
CREATE UNIQUE INDEX idx_email ON users(email);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 부분 인덱스 (Partial Index)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 조건을 만족하는 행만 인덱싱&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;용도&lt;/strong&gt;: 특정 조건의 데이터만 빠르게 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 부분 인덱스 예시 (MySQL에서는 WHERE 절 사용)
CREATE INDEX idx_active_users ON users(name) WHERE status = &amp;#39;active&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;인덱스 생성 및 관리&lt;/h2&gt;
&lt;h3&gt;1. 인덱스 생성&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 기본 인덱스 생성
CREATE INDEX idx_name ON users(name);

-- 복합 인덱스 생성
CREATE INDEX idx_name_email ON users(name, email);

-- 고유 인덱스 생성
CREATE UNIQUE INDEX idx_email ON users(email);

-- 인덱스 생성 시 정렬 지정
CREATE INDEX idx_name ON users(name DESC);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 인덱스 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 테이블의 인덱스 확인
SHOW INDEX FROM users;

-- 또는
SELECT
    INDEX_NAME,
    COLUMN_NAME,
    NON_UNIQUE,
    SEQ_IN_INDEX
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = &amp;#39;your_database&amp;#39;
AND TABLE_NAME = &amp;#39;users&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 인덱스 삭제&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 인덱스 삭제
DROP INDEX idx_name ON users;

-- 또는
ALTER TABLE users DROP INDEX idx_name;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 인덱스 재구성&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 인덱스 재구성 (데이터 정렬)
OPTIMIZE TABLE users;

-- 또는
ALTER TABLE users FORCE;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;인덱스 성능 최적화&lt;/h2&gt;
&lt;h3&gt;1. 인덱스 선택 기준&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;인덱스를 생성해야 하는 경우:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WHERE 절에서 자주 사용되는 컬럼&lt;/li&gt;
&lt;li&gt;JOIN 조건에 사용되는 컬럼&lt;/li&gt;
&lt;li&gt;ORDER BY, GROUP BY에 사용되는 컬럼&lt;/li&gt;
&lt;li&gt;DISTINCT 연산이 자주 사용되는 컬럼&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;인덱스를 생성하지 않는 것이 좋은 경우:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;자주 변경되는 컬럼&lt;/li&gt;
&lt;li&gt;NULL 값이 많은 컬럼&lt;/li&gt;
&lt;li&gt;카디널리티가 낮은 컬럼 (성별, 상태 등)&lt;/li&gt;
&lt;li&gt;이미 다른 인덱스에 포함된 컬럼&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 카디널리티 (Cardinality)&lt;/h3&gt;
&lt;p&gt;카디널리티는 컬럼의 고유 값의 개수를 의미합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 카디널리티 확인
SELECT
    COUNT(DISTINCT name) as name_cardinality,
    COUNT(DISTINCT status) as status_cardinality,
    COUNT(*) as total_rows
FROM users;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;카디널리티가 높은 컬럼&lt;/strong&gt;: 인덱스 효과가 좋음&lt;br&gt;&lt;strong&gt;카디널리티가 낮은 컬럼&lt;/strong&gt;: 인덱스 효과가 제한적&lt;/p&gt;
&lt;h3&gt;3. 인덱스 사용 여부 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- EXPLAIN을 사용한 쿼리 실행 계획 확인
EXPLAIN SELECT * FROM users WHERE name = &amp;#39;John&amp;#39;;

-- 결과 해석
-- type: index (인덱스 사용), ALL (전체 스캔)
-- key: 사용된 인덱스 이름
-- rows: 검사할 행의 수&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 인덱스 통계 정보&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 인덱스 통계 정보 확인
SELECT
    TABLE_NAME,
    INDEX_NAME,
    CARDINALITY,
    SUB_PART,
    PACKED,
    NULLABLE,
    INDEX_TYPE
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = &amp;#39;your_database&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;실제 예제&lt;/h2&gt;
&lt;h3&gt;1. 테스트 데이터 세팅&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 사용자 테이블 생성
-- 1. 사용자 테이블 생성 (인덱스 없이)
CREATE TABLE users_no_index (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    status ENUM(&amp;#39;active&amp;#39;, &amp;#39;inactive&amp;#39;, &amp;#39;suspended&amp;#39;) DEFAULT &amp;#39;active&amp;#39;,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 2. 사용자 테이블 생성 (인덱스 있음)
CREATE TABLE users_with_index (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    status ENUM(&amp;#39;active&amp;#39;, &amp;#39;inactive&amp;#39;, &amp;#39;suspended&amp;#39;) DEFAULT &amp;#39;active&amp;#39;,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 인덱스 생성
CREATE UNIQUE INDEX idx_username ON users_with_index(username);
CREATE UNIQUE INDEX idx_email ON users_with_index(email);
CREATE INDEX idx_status ON users_with_index(status);
CREATE INDEX idx_created_at ON users_with_index(created_at);
CREATE INDEX idx_status_created ON users_with_index(status, created_at);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;테스트 데이터 10만건씩 삽입&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO users_no_index (username, email, status)
SELECT
    CONCAT(&amp;#39;user&amp;#39;, LPAD(id, 6, &amp;#39;0&amp;#39;)) as username,
    CONCAT(&amp;#39;user&amp;#39;, LPAD(id, 6, &amp;#39;0&amp;#39;), &amp;#39;@example.com&amp;#39;) as email,
    CASE WHEN id % 10 = 0 THEN &amp;#39;inactive&amp;#39;
         WHEN id % 100 = 0 THEN &amp;#39;suspended&amp;#39;
         ELSE &amp;#39;active&amp;#39; END as status
FROM (
    SELECT 1 + units.i + tens.i * 10 + hundreds.i * 100 + thousands.i * 1000 + ten_thousands.i * 10000 as id
    FROM (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) units,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) tens,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) hundreds,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) thousands,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) ten_thousands
    WHERE 1 + units.i + tens.i * 10 + hundreds.i * 100 + thousands.i * 1000 + ten_thousands.i * 10000 &amp;lt;= 100000
) numbers;

INSERT INTO users_with_index (username, email, status)
SELECT
    CONCAT(&amp;#39;user&amp;#39;, LPAD(id, 6, &amp;#39;0&amp;#39;)) as username,
    CONCAT(&amp;#39;user&amp;#39;, LPAD(id, 6, &amp;#39;0&amp;#39;), &amp;#39;@example.com&amp;#39;) as email,
    CASE WHEN id % 10 = 0 THEN &amp;#39;inactive&amp;#39;
         WHEN id % 100 = 0 THEN &amp;#39;suspended&amp;#39;
         ELSE &amp;#39;active&amp;#39; END as status
FROM (
    SELECT 1 + units.i + tens.i * 10 + hundreds.i * 100 + thousands.i * 1000 + ten_thousands.i * 10000 as id
    FROM (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) units,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) tens,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) hundreds,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) thousands,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) ten_thousands
    WHERE 1 + units.i + tens.i * 10 + hundreds.i * 100 + thousands.i * 1000 + ten_thousands.i * 10000 &amp;lt;= 100000
) numbers;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index 없는 테이블
0.419 sec

index 있는 테이블
1.394 sec&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;INSERT 에서 인덱스가 있고 없고의 차이가 10만개 기준 거의 3배 차이남&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 주문 테이블 생성
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    order_number VARCHAR(50) NOT NULL,
    status ENUM(&amp;#39;pending&amp;#39;, &amp;#39;processing&amp;#39;, &amp;#39;shipped&amp;#39;, &amp;#39;delivered&amp;#39;, &amp;#39;cancelled&amp;#39;) DEFAULT &amp;#39;pending&amp;#39;,
    total_amount DECIMAL(10,2) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 인덱스 생성
CREATE UNIQUE INDEX idx_order_number ON orders(order_number);
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_status ON orders(status);
CREATE INDEX idx_created_at ON orders(created_at);
CREATE INDEX idx_user_status ON orders(user_id, status);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;50만건 테스트 데이터 추가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (user_id, order_number, status, total_amount)
SELECT
    FLOOR(1 + RAND() * 100000) as user_id,
    CONCAT(&amp;#39;ORD&amp;#39;, LPAD(id, 8, &amp;#39;0&amp;#39;)) as order_number,
    CASE WHEN id % 5 = 0 THEN &amp;#39;pending&amp;#39;
         WHEN id % 5 = 1 THEN &amp;#39;processing&amp;#39;
         WHEN id % 5 = 2 THEN &amp;#39;shipped&amp;#39;
         WHEN id % 5 = 3 THEN &amp;#39;delivered&amp;#39;
         ELSE &amp;#39;cancelled&amp;#39; END as status,
    ROUND(RAND() * 1000, 2) as total_amount
FROM (
    SELECT 1 + units.i + tens.i * 10 + hundreds.i * 100 + thousands.i * 1000 + ten_thousands.i * 10000 + hundred_thousands.i * 100000 as id
    FROM (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) units,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) tens,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) hundreds,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) thousands,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) ten_thousands,
         (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) hundred_thousands
    WHERE 1 + units.i + tens.i * 10 + hundreds.i * 100 + thousands.i * 1000 + ten_thousands.i * 10000 + hundred_thousands.i * 100000 &amp;lt;= 500000
) numbers;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 성능 테스트&lt;/h3&gt;
&lt;h4&gt;조회 성능 비교&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 인덱스 성능 비교
-- 인덱스 없는 쿼리
EXPLAIN SELECT * FROM users_no_index WHERE username = &amp;#39;user000001&amp;#39;;

-- 인덱스 있는 쿼리
EXPLAIN SELECT * FROM users_with_index WHERE username = &amp;#39;user000001&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index 없는 테이블
0.0021 sec

index 있는 테이블
0.00074 sec&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;INSERT 와는 반대로 인덱스가 있는 테이블이 거의 3배 빨랐다.&lt;/p&gt;
&lt;h4&gt;복합 인덱스 테스트&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 2-1. 상태와 생성일로 검색 (복합 인덱스 활용)
EXPLAIN SELECT * FROM users_with_index
WHERE status = &amp;#39;active&amp;#39; AND created_at &amp;gt; &amp;#39;2024-01-01&amp;#39;
ORDER BY created_at DESC;

-- 2-2. 상태만으로 검색 (복합 인덱스 부분 활용)
EXPLAIN SELECT * FROM users_with_index
WHERE status = &amp;#39;active&amp;#39;
ORDER BY created_at DESC;

-- 2-3. 생성일만으로 검색 (복합 인덱스 미사용)
EXPLAIN SELECT * FROM users_with_index
WHERE created_at &amp;gt; &amp;#39;2024-01-01&amp;#39;
ORDER BY created_at DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;JOIN 성능 테스트&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 사용자와 주문 조인 (인덱스 활용)
EXPLAIN SELECT u.username, o.order_number, o.total_amount
FROM users_with_index u
JOIN orders o ON u.id = o.user_id
WHERE u.status = &amp;#39;active&amp;#39; AND o.status = &amp;#39;delivered&amp;#39;
ORDER BY o.created_at DESC
LIMIT 100;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;카디널리티 테스트&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 높은 카디널리티 컬럼 (username) 검색
EXPLAIN SELECT * FROM users_with_index WHERE username LIKE &amp;#39;user%&amp;#39;;

-- 낮은 카디널리티 컬럼 (status) 검색
EXPLAIN SELECT * FROM users_with_index WHERE status = &amp;#39;active&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;정렬 성능 테스트&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 인덱스가 있는 컬럼으로 정렬
EXPLAIN SELECT * FROM users_with_index
WHERE status = &amp;#39;active&amp;#39;
ORDER BY created_at DESC
LIMIT 1000;

-- 인덱스가 없는 컬럼으로 정렬
EXPLAIN SELECT * FROM users_with_index
WHERE status = &amp;#39;active&amp;#39;
ORDER BY email
LIMIT 1000;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;집계 함수 성능 테스트&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 상태별 사용자 수 (인덱스 활용)
EXPLAIN SELECT status, COUNT(*) as user_count
FROM users_with_index
GROUP BY status;

-- 월별 주문 수 (인덱스 활용)
EXPLAIN SELECT
    DATE_FORMAT(created_at, &amp;#39;%Y-%m&amp;#39;) as month,
    COUNT(*) as order_count,
    SUM(total_amount) as total_sales
FROM orders
GROUP BY DATE_FORMAT(created_at, &amp;#39;%Y-%m&amp;#39;)
ORDER BY month;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;인덱스 사용 통계 확인&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 테이블별 인덱스 정보
SELECT
    TABLE_NAME,
    INDEX_NAME,
    COLUMN_NAME,
    CARDINALITY,
    NON_UNIQUE,
    SEQ_IN_INDEX
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = &amp;#39;index_test&amp;#39;
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX;

-- 인덱스 크기 확인
SELECT
    TABLE_NAME,
    ROUND(SUM(INDEX_LENGTH) / 1024 / 1024, 2) as index_size_mb
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = &amp;#39;index_test&amp;#39;
GROUP BY TABLE_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;성능 모니터링&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    DIGEST_TEXT as query,
    COUNT_STAR as exec_count,
    ROUND(AVG_TIMER_WAIT/1000000000, 3) as avg_time_sec,
    ROUND(SUM_TIMER_WAIT/1000000000, 3) as total_time_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = &amp;#39;index_test&amp;#39;
AND AVG_TIMER_WAIT &amp;gt; 1000000000  -- 1초 이상
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;인덱스 최적화 테스트&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 불필요한 인덱스 확인
SELECT
    s.TABLE_NAME,
    s.INDEX_NAME,
    s.CARDINALITY,
    t.TABLE_ROWS,
    ROUND(s.CARDINALITY / t.TABLE_ROWS * 100, 2) as selectivity_percent
FROM INFORMATION_SCHEMA.STATISTICS s
JOIN INFORMATION_SCHEMA.TABLES t ON s.TABLE_NAME = t.TABLE_NAME
WHERE s.TABLE_SCHEMA = &amp;#39;index_test&amp;#39;
AND t.TABLE_SCHEMA = &amp;#39;index_test&amp;#39;
AND s.SEQ_IN_INDEX = 1  -- 첫 번째 컬럼만
ORDER BY selectivity_percent;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;실제 성능 측정&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 인덱스 없는 테이블 성능 측정
SET profiling = 1;
SELECT SQL_NO_CACHE * FROM users_no_index WHERE username = &amp;#39;user000001&amp;#39;;
SHOW PROFILES;

-- 인덱스 있는 테이블 성능 측정
SELECT SQL_NO_CACHE * FROM users_with_index WHERE username = &amp;#39;user000001&amp;#39;;
SHOW PROFILES;

SET profiling = 0;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;인덱스 사용 현황 분석&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 각 인덱스의 사용 빈도 (MySQL 8.0+)
SELECT
    OBJECT_SCHEMA as database_name,
    OBJECT_NAME as table_name,
    INDEX_NAME,
    COUNT_READ,
    COUNT_WRITE,
    COUNT_FETCH,
    COUNT_INSERT,
    COUNT_UPDATE,
    COUNT_DELETE
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = &amp;#39;index_test&amp;#39;
ORDER BY COUNT_READ DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;주의사항 및 모범 사례&lt;/h2&gt;
&lt;h3&gt;1. 인덱스 과다 사용 방지&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;너무 많은 인덱스는 INSERT, UPDATE, DELETE 성능 저하&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 정기적인 인덱스 분석&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 인덱스 사용 통계 확인
SELECT
    TABLE_NAME,
    INDEX_NAME,
    CARDINALITY,
    SUB_PART
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = &amp;#39;index_test&amp;#39;
ORDER BY CARDINALITY DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 인덱스 유지보수&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 정기적인 인덱스 재구성
OPTIMIZE TABLE users_with_index;
OPTIMIZE TABLE orders;

-- 인덱스 통계 업데이트
ANALYZE TABLE users_with_index;
ANALYZE TABLE orders;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 모니터링 쿼리&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 느린 쿼리 확인
SELECT
    query,
    exec_count,
    avg_timer_wait/1000000000 as avg_time_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE avg_timer_wait &amp;gt; 1000000000  -- 1초 이상
ORDER BY avg_timer_wait DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;MySQL 인덱스는 데이터베이스 성능 최적화의 핵심 요소입니다. 적절한 인덱스 설계와 관리를 통해 쿼리 성능을 크게 향상시킬 수 있습니다. 하지만 과도한 인덱스는 오히려 성능을 저하시킬 수 있으므로, 실제 사용 패턴을 분석하여 필요한 인덱스만 선별적으로 생성하는 것이 중요합니다.&lt;/p&gt;
&lt;h3&gt;핵심 포인트&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;B-Tree 구조 이해&lt;/strong&gt;: 인덱스의 기본 작동 원리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;카디널리티 고려&lt;/strong&gt;: 높은 카디널리티 컬럼 우선&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;복합 인덱스 순서&lt;/strong&gt;: 자주 사용되는 컬럼을 앞에 배치&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정기적인 모니터링&lt;/strong&gt;: 인덱스 사용 현황 파악&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;실제 데이터로 테스트&lt;/strong&gt;: 이론과 실제 성능의 차이 확인&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Database</category>
      <author>TeTedo.</author>
      <guid isPermaLink="true">https://diary-blockchain.tistory.com/387</guid>
      <comments>https://diary-blockchain.tistory.com/387#entry387comment</comments>
      <pubDate>Sun, 13 Jul 2025 21:06:50 +0900</pubDate>
    </item>
    <item>
      <title>[DB] - MySQL B-Tree</title>
      <link>https://diary-blockchain.tistory.com/386</link>
      <description>&lt;h1&gt;MySQL B-Tree 배워보기&lt;/h1&gt;
&lt;p&gt;모든 코드는 &lt;a href=&quot;https://github.com/TeTedo/blog-code/tree/main/db-mysql-binary-tree&quot;&gt;깃허브&lt;/a&gt;에서 볼수 있습니다.&lt;/p&gt;
&lt;h2&gt;1. B-Tree란?&lt;/h2&gt;
&lt;p&gt;B-Tree는 데이터베이스 인덱싱에서 가장 널리 사용되는 자료구조입니다. 이진트리를 확장한 형태로, 각 노드가 여러 개의 자식을 가질 수 있는 균형잡힌 트리입니다.&lt;/p&gt;
&lt;h3&gt;B-Tree의 특징&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;각 노드는 여러 개의 키를 가질 수 있음&lt;/li&gt;
&lt;li&gt;모든 리프 노드가 같은 레벨에 있음 (균형잡힌 구조)&lt;/li&gt;
&lt;li&gt;검색, 삽입, 삭제 연산이 모두 O(log n) 시간복잡도&lt;/li&gt;
&lt;li&gt;디스크 기반 저장소에 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;B-Tree vs 이진트리&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;이진트리&lt;/strong&gt;: 각 노드가 최대 2개의 자식&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B-Tree&lt;/strong&gt;: 각 노드가 여러 개의 자식 (보통 수백~수천개)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B-Tree&lt;/strong&gt;: 디스크 I/O 최소화에 특화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;B-Tree의 핵심 메커니즘&lt;/h3&gt;
&lt;h4&gt;1. 노드 분할 (Node Splitting)&lt;/h4&gt;
&lt;p&gt;B-Tree에서 노드가 가득 찰 때 발생하는 핵심 연산입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;분할 과정:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;중간 키 선택&lt;/strong&gt;: 노드의 중간 키를 선택&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;키 분배&lt;/strong&gt;: 중간 키보다 작은 키들은 왼쪽, 큰 키들은 오른쪽으로 분배&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부모 노드 업데이트&lt;/strong&gt;: 중간 키를 부모 노드로 이동&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;균형 유지&lt;/strong&gt;: 모든 리프 노드가 같은 레벨에 있도록 유지&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;예시:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;분할 전: [3, 5, 7, 9, 11] (가득 찬 노드)
분할 후:
  부모: [7]
  왼쪽: [3, 5]
  오른쪽: [9, 11]&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;2. 재조정 (Rebalancing)&lt;/h4&gt;
&lt;p&gt;B-Tree가 균형을 유지하기 위한 연산입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;재조정이 필요한 경우:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;노드가 최소 키 수보다 적어질 때&lt;/li&gt;
&lt;li&gt;삭제 연산 후 노드가 비어있을 때&lt;/li&gt;
&lt;li&gt;형제 노드에서 키를 빌려올 수 있을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;재조정 과정:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;형제 노드 확인&lt;/strong&gt;: 같은 부모를 가진 형제 노드들 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;키 재분배&lt;/strong&gt;: 형제 노드에서 키를 빌려와서 균형 조정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부모 노드 조정&lt;/strong&gt;: 부모 노드의 키 값 업데이트&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;병합 (필요시)&lt;/strong&gt;: 형제 노드에서 키를 빌릴 수 없으면 노드 병합&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;예시:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;재조정 전:
  부모: [10]
  왼쪽: [3] (최소 키 수 미달)
  오른쪽: [15, 18, 20]

재조정 후:
  부모: [15]
  왼쪽: [3, 10]
  오른쪽: [18, 20]&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;3. B-Tree의 균형 유지 원리&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;균형 조건:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모든 리프 노드는 같은 레벨에 있어야 함&lt;/li&gt;
&lt;li&gt;각 노드는 최소 &lt;code&gt;⌈degree/2⌉ - 1&lt;/code&gt;개의 키를 가져야 함&lt;/li&gt;
&lt;li&gt;각 노드는 최대 &lt;code&gt;degree - 1&lt;/code&gt;개의 키를 가질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;균형 유지의 장점:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;일관된 성능&lt;/strong&gt;: 모든 검색이 동일한 깊이에서 완료&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예측 가능한 I/O&lt;/strong&gt;: 디스크 접근 횟수가 일정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;효율적인 범위 검색&lt;/strong&gt;: 연속된 키들을 빠르게 찾기 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;B-Tree의 실제 동작 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;초기 상태: [10]
         /   \
      [5]   [15]

키 3 삽입: [10]
          /   \
       [3,5] [15]

키 7 삽입: [10]
          /   \
       [3,5,7] [15]

키 12 삽입: [10]
           /   \
        [3,5,7] [12,15]

키 18 삽입: [10, 15]
           /   |   \
        [3,5,7] [12] [18]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이러한 노드 분할과 재조정을 통해 B-Tree는 항상 균형잡힌 상태를 유지하며, 대용량 데이터에서도 일관된 성능을 제공합니다.&lt;/p&gt;
&lt;h2&gt;2. MySQL에서 B-Tree 사용하기&lt;/h2&gt;
&lt;h3&gt;B-Tree 인덱스&lt;/h3&gt;
&lt;p&gt;MySQL의 기본 인덱스 구조는 B-Tree입니다. InnoDB 스토리지 엔진에서는 B+Tree를 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 인덱스 생성 예시
CREATE INDEX idx_user_email ON users(email);
CREATE INDEX idx_product_category ON products(category_id);
CREATE INDEX idx_order_date ON orders(order_date);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;검색 성능 최적화&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;O(log n) 시간복잡도&lt;/strong&gt;: 대용량 데이터에서도 빠른 검색&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;범위 검색&lt;/strong&gt;: BETWEEN, &amp;lt;, &amp;gt; 등의 연산에 효율적&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정렬된 데이터&lt;/strong&gt;: 자동으로 정렬된 상태 유지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디스크 I/O 최소화&lt;/strong&gt;: 한 번의 디스크 읽기로 많은 데이터 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;실제 MySQL 동작 과정&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 이 쿼리가 실행될 때
SELECT * FROM users WHERE email = &amp;#39;user@example.com&amp;#39;;

-- MySQL은 다음과 같이 동작합니다:
-- 1. email 컬럼의 B-Tree 인덱스 검색
-- 2. O(log n) 시간에 해당 레코드 위치 찾기
-- 3. 해당 위치에서 실제 데이터 조회
-- 4. 디스크 I/O 최소화로 성능 향상&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. JavaScript로 B-Tree 구현해보기&lt;/h2&gt;
&lt;h3&gt;B-Node 노드 구조&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class BTreeNode {
  constructor(isLeaf = true) {
    this.isLeaf = isLeaf;
    this.keys = [];
    this.children = [];
    this.next = null; // B+Tree를 위한 링크
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;B-Tree 구현&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class BTree {
  constructor(degree = 3) {
    this.root = new BTreeNode(true);
    this.degree = degree;
    this.minKeys = Math.ceil(degree / 2) - 1;
    this.maxKeys = degree - 1;
  }

  // 노드 검색
  search(key) {
    return this._searchNode(this.root, key);
  }

  _searchNode(node, key) {
    let i = 0;

    // 키를 찾을 위치 찾기
    while (i &amp;lt; node.keys.length &amp;amp;&amp;amp; key &amp;gt; node.keys[i]) {
      i++;
    }

    // 리프 노드에서 찾기
    if (node.isLeaf) {
      if (i &amp;lt; node.keys.length &amp;amp;&amp;amp; node.keys[i] === key) {
        return node;
      }
      return null;
    }

    // 내부 노드에서 자식으로 이동
    return this._searchNode(node.children[i], key);
  }

  // 노드 삽입
  insert(key) {
    const root = this.root;

    // 루트가 가득 찬 경우
    if (root.keys.length === this.maxKeys) {
      const newRoot = new BTreeNode(false);
      newRoot.children.push(root);
      this._splitChild(newRoot, 0);
      this.root = newRoot;
    }

    this._insertNonFull(this.root, key);
  }

  _insertNonFull(node, key) {
    let i = node.keys.length - 1;

    // 리프 노드인 경우
    if (node.isLeaf) {
      while (i &amp;gt;= 0 &amp;amp;&amp;amp; key &amp;lt; node.keys[i]) {
        node.keys[i + 1] = node.keys[i];
        i--;
      }
      node.keys[i + 1] = key;
    } else {
      // 내부 노드인 경우
      while (i &amp;gt;= 0 &amp;amp;&amp;amp; key &amp;lt; node.keys[i]) {
        i--;
      }
      i++;

      if (node.children[i].keys.length === this.maxKeys) {
        this._splitChild(node, i);
        if (key &amp;gt; node.keys[i]) {
          i++;
        }
      }
      this._insertNonFull(node.children[i], key);
    }
  }

  _splitChild(parent, childIndex) {
    const child = parent.children[childIndex];
    const newNode = new BTreeNode(child.isLeaf);

    // 키 분할
    const midIndex = Math.floor(child.keys.length / 2);
    const midKey = child.keys[midIndex];

    // 오른쪽 노드에 키 이동
    for (let i = midIndex + 1; i &amp;lt; child.keys.length; i++) {
      newNode.keys.push(child.keys[i]);
    }
    child.keys = child.keys.slice(0, midIndex);

    // 자식 노드 분할 (내부 노드인 경우)
    if (!child.isLeaf) {
      for (let i = midIndex + 1; i &amp;lt; child.children.length; i++) {
        newNode.children.push(child.children[i]);
      }
      child.children = child.children.slice(0, midIndex + 1);
    }

    // 부모 노드에 중간 키 삽입
    parent.keys.splice(childIndex, 0, midKey);
    parent.children.splice(childIndex + 1, 0, newNode);
  }

  // 범위 검색
  rangeSearch(minKey, maxKey) {
    const result = [];
    this._rangeSearchNode(this.root, minKey, maxKey, result);
    return result;
  }

  _rangeSearchNode(node, minKey, maxKey, result) {
    let i = 0;

    // 현재 노드의 키들 확인
    while (i &amp;lt; node.keys.length) {
      if (node.isLeaf) {
        if (node.keys[i] &amp;gt;= minKey &amp;amp;&amp;amp; node.keys[i] &amp;lt;= maxKey) {
          result.push(node.keys[i]);
        }
      } else {
        // 내부 노드인 경우 자식으로 재귀
        if (i === 0 || node.keys[i - 1] &amp;lt; minKey) {
          this._rangeSearchNode(node.children[i], minKey, maxKey, result);
        }
      }
      i++;
    }

    // 마지막 자식 확인
    if (!node.isLeaf) {
      this._rangeSearchNode(node.children[i], minKey, maxKey, result);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MySQL 인덱스 시뮬레이션&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class MySQLBTreeSimulator {
  constructor() {
    this.btree = new BTree(4); // degree = 4
    this.data = new Map(); // 실제 데이터 저장
  }

  // 데이터 삽입 (MySQL INSERT)
  insertRecord(id, data) {
    this.btree.insert(id);
    this.data.set(id, data);
    console.log(`레코드 삽입: ID=${id}, 데이터=${JSON.stringify(data)}`);
  }

  // 데이터 검색 (MySQL SELECT)
  searchRecord(id) {
    const found = this.btree.search(id);
    if (found) {
      const data = this.data.get(id);
      console.log(`레코드 검색 성공: ID=${id}, 데이터=${JSON.stringify(data)}`);
      return data;
    } else {
      console.log(`레코드 검색 실패: ID=${id}를 찾을 수 없습니다.`);
      return null;
    }
  }

  // 범위 검색 (MySQL BETWEEN)
  searchRange(minId, maxId) {
    const result = this.btree.rangeSearch(minId, maxId);

    console.log(`범위 검색: ${minId} ~ ${maxId}`);
    result.forEach((id) =&amp;gt; {
      const data = this.data.get(id);
      console.log(`  ID=${id}, 데이터=${JSON.stringify(data)}`);
    });

    return result;
  }

  // 인덱스 통계 출력
  printIndexStats() {
    console.log(`\n=== B-Tree 인덱스 통계 ===`);
    console.log(`차수(degree): ${this.btree.degree}`);
    console.log(`최소 키 수: ${this.btree.minKeys}`);
    console.log(`최대 키 수: ${this.btree.maxKeys}`);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 실행해보기&lt;/h2&gt;
&lt;h3&gt;실제 사용 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&amp;quot;  MySQL B-Tree 시뮬레이션 시작\n&amp;quot;);

// MySQL B-Tree 시뮬레이션 실행
const mysqlSimulator = new MySQLBTreeSimulator();

console.log(&amp;quot;  데이터 삽입 테스트&amp;quot;);
// 데이터 삽입 (INSERT)
mysqlSimulator.insertRecord(10, {
  name: &amp;quot;김철수&amp;quot;,
  email: &amp;quot;kim@example.com&amp;quot;,
  age: 25,
});
mysqlSimulator.insertRecord(5, {
  name: &amp;quot;이영희&amp;quot;,
  email: &amp;quot;lee@example.com&amp;quot;,
  age: 28,
});
mysqlSimulator.insertRecord(15, {
  name: &amp;quot;박민수&amp;quot;,
  email: &amp;quot;park@example.com&amp;quot;,
  age: 32,
});
mysqlSimulator.insertRecord(3, {
  name: &amp;quot;정수진&amp;quot;,
  email: &amp;quot;jung@example.com&amp;quot;,
  age: 24,
});
mysqlSimulator.insertRecord(7, {
  name: &amp;quot;최지영&amp;quot;,
  email: &amp;quot;choi@example.com&amp;quot;,
  age: 29,
});
mysqlSimulator.insertRecord(12, {
  name: &amp;quot;한민호&amp;quot;,
  email: &amp;quot;han@example.com&amp;quot;,
  age: 31,
});
mysqlSimulator.insertRecord(18, {
  name: &amp;quot;송미영&amp;quot;,
  email: &amp;quot;song@example.com&amp;quot;,
  age: 26,
});

console.log(&amp;quot;\n  개별 검색 테스트&amp;quot;);
mysqlSimulator.searchRecord(7); // 성공
mysqlSimulator.searchRecord(9); // 실패

console.log(&amp;quot;\n  범위 검색 테스트&amp;quot;);
mysqlSimulator.searchRange(5, 12);

// 인덱스 통계 출력
mysqlSimulator.printIndexStats();

console.log(&amp;quot;\n  트리 구조 시각화&amp;quot;);
mysqlSimulator.visualizeTree();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;터미널에 다음과 같이 출력된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;  데이터 삽입 테스트
레코드 삽입: ID=10, 데이터={&amp;quot;name&amp;quot;:&amp;quot;김철수&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;kim@example.com&amp;quot;,&amp;quot;age&amp;quot;:25}
레코드 삽입: ID=5, 데이터={&amp;quot;name&amp;quot;:&amp;quot;이영희&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;lee@example.com&amp;quot;,&amp;quot;age&amp;quot;:28}
레코드 삽입: ID=15, 데이터={&amp;quot;name&amp;quot;:&amp;quot;박민수&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;park@example.com&amp;quot;,&amp;quot;age&amp;quot;:32}
레코드 삽입: ID=3, 데이터={&amp;quot;name&amp;quot;:&amp;quot;정수진&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;jung@example.com&amp;quot;,&amp;quot;age&amp;quot;:24}
레코드 삽입: ID=7, 데이터={&amp;quot;name&amp;quot;:&amp;quot;최지영&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;choi@example.com&amp;quot;,&amp;quot;age&amp;quot;:29}
레코드 삽입: ID=12, 데이터={&amp;quot;name&amp;quot;:&amp;quot;한민호&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;han@example.com&amp;quot;,&amp;quot;age&amp;quot;:31}
레코드 삽입: ID=18, 데이터={&amp;quot;name&amp;quot;:&amp;quot;송미영&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;song@example.com&amp;quot;,&amp;quot;age&amp;quot;:26}

  개별 검색 테스트
레코드 검색 성공: ID=7, 데이터={&amp;quot;name&amp;quot;:&amp;quot;최지영&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;choi@example.com&amp;quot;,&amp;quot;age&amp;quot;:29}
레코드 검색 실패: ID=9를 찾을 수 없습니다.

  범위 검색 테스트
범위 검색: 5 ~ 12
  ID=5, 데이터={&amp;quot;name&amp;quot;:&amp;quot;이영희&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;lee@example.com&amp;quot;,&amp;quot;age&amp;quot;:28}
  ID=7, 데이터={&amp;quot;name&amp;quot;:&amp;quot;최지영&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;choi@example.com&amp;quot;,&amp;quot;age&amp;quot;:29}
  ID=12, 데이터={&amp;quot;name&amp;quot;:&amp;quot;한민호&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;han@example.com&amp;quot;,&amp;quot;age&amp;quot;:31}

=== B-Tree 인덱스 통계 ===
차수(degree): 4
최소 키 수: 1
최대 키 수: 3
총 레코드 수: 7
정렬된 키 목록: [3, 5, 7, 10, 12, 15, 18]

  트리 구조 시각화
=== B-Tree 구조 ===
└── [10]
    ┌── [3,5,7]
    └── [12,15,18]&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 성능 비교&lt;/h2&gt;
&lt;h3&gt;시간복잡도 비교&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;연산&lt;/th&gt;
&lt;th&gt;배열 (순차검색)&lt;/th&gt;
&lt;th&gt;이진트리&lt;/th&gt;
&lt;th&gt;B-Tree&lt;/th&gt;
&lt;th&gt;MySQL B-Tree&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;검색&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삽입&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;범위검색&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(k + log n)&lt;/td&gt;
&lt;td&gt;O(k + log n)&lt;/td&gt;
&lt;td&gt;O(k + log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;디스크 I/O 비교&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;이진트리&lt;/strong&gt;: 불균형할 수 있어 디스크 I/O 많음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B-Tree&lt;/strong&gt;: 균형잡힌 구조로 디스크 I/O 최소화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL B-Tree&lt;/strong&gt;: 페이지 단위로 데이터 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;실제 성능 테스트&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const { BTree } = require(&amp;quot;./b-tree.js&amp;quot;);

function performanceTest() {
  const btree = new BTree(4);
  const array = [];

  console.log(&amp;quot;=== B-Tree 성능 테스트 ===&amp;quot;);

  // 데이터 크기를 늘려서 더 유의미한 테스트
  const dataSize = 100000; // 10만개 데이터

  // 데이터 삽입 성능
  const insertStart = Date.now();
  for (let i = 0; i &amp;lt; dataSize; i++) {
    btree.insert(i);
    array.push(i);
  }
  const insertEnd = Date.now();
  console.log(
    `B-Tree 삽입 시간: ${insertEnd - insertStart}ms (${dataSize}개 데이터)`
  );

  // 검색 성능 비교 - 여러 번 반복해서 평균 측정
  const searchValue = Math.floor(dataSize / 2); // 중간 값 검색
  const iterations = 1000; // 1000번 반복

  // B-Tree 검색 (반복)
  const btreeSearchStart = Date.now();
  for (let i = 0; i &amp;lt; iterations; i++) {
    btree.search(searchValue);
  }
  const btreeSearchEnd = Date.now();

  // 배열 순차검색 (반복)
  const arraySearchStart = Date.now();
  for (let i = 0; i &amp;lt; iterations; i++) {
    array.indexOf(searchValue);
  }
  const arraySearchEnd = Date.now();

  const btreeSearchTime = btreeSearchEnd - btreeSearchStart;
  const arraySearchTime = arraySearchEnd - arraySearchStart;

  console.log(
    `B-Tree 검색 시간: ${btreeSearchTime}ms (${iterations}번 반복, 평균: ${(
      btreeSearchTime / iterations
    ).toFixed(3)}ms)`
  );
  console.log(
    `배열 검색 시간: ${arraySearchTime}ms (${iterations}번 반복, 평균: ${(
      arraySearchTime / iterations
    ).toFixed(3)}ms)`
  );

  // 성능 비교
  if (btreeSearchTime === 0) {
    console.log(`성능 차이: B-Tree 검색이 너무 빠름 (0ms)`);
  } else {
    const performanceRatio = arraySearchTime / btreeSearchTime;
    console.log(`성능 차이: B-Tree가 ${performanceRatio.toFixed(2)}배 빠름`);
  }

  // 추가 테스트: 최악의 경우 (배열 끝에서 검색)
  console.log(&amp;quot;\n=== 최악의 경우 테스트 (배열 끝에서 검색) ===&amp;quot;);
  const worstCaseValue = dataSize - 1;

  const btreeWorstStart = Date.now();
  for (let i = 0; i &amp;lt; iterations; i++) {
    btree.search(worstCaseValue);
  }
  const btreeWorstEnd = Date.now();

  const arrayWorstStart = Date.now();
  for (let i = 0; i &amp;lt; iterations; i++) {
    array.indexOf(worstCaseValue);
  }
  const arrayWorstEnd = Date.now();

  const btreeWorstTime = btreeWorstEnd - btreeWorstStart;
  const arrayWorstTime = arrayWorstEnd - arrayWorstStart;

  console.log(`B-Tree 최악의 경우: ${btreeWorstTime}ms (${iterations}번 반복)`);
  console.log(`배열 최악의 경우: ${arrayWorstTime}ms (${iterations}번 반복)`);

  if (btreeWorstTime === 0) {
    console.log(`최악의 경우 성능 차이: B-Tree 검색이 너무 빠름`);
  } else {
    const worstCaseRatio = arrayWorstTime / btreeWorstTime;
    console.log(
      `최악의 경우 성능 차이: B-Tree가 ${worstCaseRatio.toFixed(2)}배 빠름`
    );
  }

  // 범위 검색 테스트
  console.log(&amp;quot;\n=== 범위 검색 테스트 ===&amp;quot;);
  const rangeStart = Math.floor(dataSize * 0.3);
  const rangeEnd = Math.floor(dataSize * 0.7);

  const btreeRangeStart = Date.now();
  for (let i = 0; i &amp;lt; 100; i++) {
    // 범위 검색은 더 적게 반복
    btree.rangeSearch(rangeStart, rangeEnd);
  }
  const btreeRangeEnd = Date.now();

  const arrayRangeStart = Date.now();
  for (let i = 0; i &amp;lt; 100; i++) {
    array.filter((x) =&amp;gt; x &amp;gt;= rangeStart &amp;amp;&amp;amp; x &amp;lt;= rangeEnd);
  }
  const arrayRangeEnd = Date.now();

  const btreeRangeTime = btreeRangeEnd - btreeRangeStart;
  const arrayRangeTime = arrayRangeEnd - arrayRangeStart;

  console.log(
    `B-Tree 범위 검색: ${btreeRangeTime}ms (100번 반복, ${
      rangeEnd - rangeStart
    }개 데이터)`
  );
  console.log(
    `배열 범위 검색: ${arrayRangeTime}ms (100번 반복, ${
      rangeEnd - rangeStart
    }개 데이터)`
  );

  if (btreeRangeTime === 0) {
    console.log(`범위 검색 성능 차이: B-Tree가 너무 빠름`);
  } else {
    const rangeRatio = arrayRangeTime / btreeRangeTime;
    console.log(
      `범위 검색 성능 차이: B-Tree가 ${rangeRatio.toFixed(2)}배 빠름`
    );
  }
}

module.exports = { performanceTest };&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=== B-Tree 성능 테스트 ===
B-Tree 삽입 시간: 45ms (100000개 데이터)
B-Tree 검색 시간: 1ms (1000번 반복, 평균: 0.001ms)
배열 검색 시간: 10ms (1000번 반복, 평균: 0.010ms)
성능 차이: B-Tree가 10.00배 빠름

=== 최악의 경우 테스트 (배열 끝에서 검색) ===
B-Tree 최악의 경우: 0ms (1000번 반복)
배열 최악의 경우: 19ms (1000번 반복)
최악의 경우 성능 차이: B-Tree 검색이 너무 빠름

=== 범위 검색 테스트 ===
B-Tree 범위 검색: 84ms (100번 반복, 40000개 데이터)
배열 범위 검색: 88ms (100번 반복, 40000개 데이터)
범위 검색 성능 차이: B-Tree가 1.05배 빠름&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;6. 뜯어보기&lt;/h2&gt;
&lt;h3&gt;B-Tree 구조&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;        [7]
       /   \
   [3,5]   [10,15]
   /  \     /    \
[1,2] [4] [8,9] [12,18]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 구조에서 각 노드는 여러 개의 키를 가질 수 있고, 모든 리프 노드가 같은 레벨에 있습니다.&lt;/p&gt;
&lt;h3&gt;MySQL B-Tree 동작 원리&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;인덱스 생성&lt;/strong&gt;: CREATE INDEX로 B-Tree 구조 생성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;페이지 관리&lt;/strong&gt;: 디스크를 페이지 단위로 관리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;검색&lt;/strong&gt;: WHERE 조건에 맞는 레코드를 B-Tree에서 빠르게 찾기&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 조회&lt;/strong&gt;: 찾은 위치에서 실제 테이블 데이터 조회&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;B-Tree의 장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;균형잡힌 구조&lt;/strong&gt;: 모든 리프 노드가 같은 레벨&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디스크 I/O 최소화&lt;/strong&gt;: 한 페이지에 많은 키 저장&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;범위 검색 효율성&lt;/strong&gt;: 연속된 키들을 빠르게 검색&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;삽입/삭제 안정성&lt;/strong&gt;: 재구성 없이 균형 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;핵심 포인트&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;효율적인 검색&lt;/strong&gt;: O(log n) 시간복잡도로 대용량 데이터에서도 빠른 검색&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;범위 검색 최적화&lt;/strong&gt;: BETWEEN, &amp;lt;, &amp;gt; 연산에 유리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디스크 최적화&lt;/strong&gt;: 페이지 단위로 데이터 관리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자동 정렬&lt;/strong&gt;: 데이터가 자동으로 정렬된 상태 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7. 결론&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;B-Tree 이해
MySQL 인덱스 활용
디스크 I/O 최적화 완료&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MySQL에서 B-Tree는 인덱싱의 핵심 자료구조로 사용됩니다. 이진트리보다 디스크 I/O를 최소화하고, 대용량 데이터에서도 안정적인 성능을 제공합니다.&lt;/p&gt;
&lt;p&gt;B-Tree의 이해는 데이터베이스 성능 최적화와 효율적인 쿼리 작성에 필수적입니다.&lt;/p&gt;</description>
      <category>개발/Database</category>
      <author>TeTedo.</author>
      <guid isPermaLink="true">https://diary-blockchain.tistory.com/386</guid>
      <comments>https://diary-blockchain.tistory.com/386#entry386comment</comments>
      <pubDate>Fri, 11 Jul 2025 19:19:14 +0900</pubDate>
    </item>
  </channel>
</rss>