<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>iseunghan</title>
    <link>https://iseunghan.tistory.com/</link>
    <description>꾸준하게 열심히..</description>
    <language>ko</language>
    <pubDate>Sun, 5 Apr 2026 08:22:28 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>iseunghan</managingEditor>
    <image>
      <title>iseunghan</title>
      <url>https://tistory1.daumcdn.net/tistory/3015229/attach/ddc85860ddef49808f5d5a57b2d8630a</url>
      <link>https://iseunghan.tistory.com</link>
    </image>
    <item>
      <title>Coroutine Deep Dive 동작 방식 정리 (feat. 바이트코드 수준에서 이해하기)</title>
      <link>https://iseunghan.tistory.com/486</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목표&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코루틴에 동작방식을 이해할 수 있다.&lt;/li&gt;
&lt;li&gt;간단한 파이썬 바이트 코드를 다룰 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Value Stack&lt;/code&gt;, &lt;code&gt;Call Stack&lt;/code&gt;, &lt;code&gt;Frame&lt;/code&gt; 객체에 대해서 얇고 넓게 배운다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Coroutine이란 무엇인가?&lt;/h2&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;python 3.10.14&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 간단한 코루틴 예제가 있습니다. 실행 결과는 어떻게 될까요?&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# coroutine.py
import asyncio

async def coroutine1():
    print(&quot;coroutine1 first entry point&quot;)
    await asyncio.sleep(1)
    print(&quot;coroutine1 second entry point&quot;)

async def coroutine2():
    print(&quot;coroutine2 first entry point&quot;)
    await asyncio.sleep(2)
    print(&quot;coroutine2 second entry point&quot;)

loop = asyncio.get_event_loop()
loop.create_task(coroutine1())
loop.create_task(coroutine2())
loop.run_forever()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과:&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# coroutine1 first entry point
# coroutine2 first entry point
# coroutine1 second entry point
# coroutine2 second entry point&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과를 보면 coroutine1과 coroutine2가 섞여서(?) 출력이 되었습니다. 왜 이렇게 동작하는지에 대해서 완벽하게 이해하는게 목표입니다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Resuming &amp;amp; Suspending&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴을 알기 위해서는 실행(또는 이전 지점 재개)과 일시중지로 작동하는 것을 알아야 합니다.&lt;br /&gt;이전 예제를 다시 살펴봅시다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;# coroutine.py
async def coroutine1():
-&amp;gt;  print(&quot;coroutine1 first entry point&quot;)
&amp;lt;-  await asyncio.sleep(1)
-&amp;gt;  print(&quot;coroutine1 second entry point&quot;)

async def coroutine2():
-&amp;gt;  print(&quot;coroutine2 first entry point&quot;)
&amp;lt;-  await asyncio.sleep(2)
-&amp;gt;  print(&quot;coroutine2 second entry point&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;-&amp;gt;&lt;/code&gt;: Resuming (실행 또는 재개)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;-&lt;/code&gt;: Suspending (일시중지)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과를 보면, coroutine1 함수의 첫 번째 print문이 실행되고, 그 다음 라인에 await를 만나 1초동안 일시정지 상태가 됩니다. 마찬가지로 coroutine2 함수의 첫 번째 print문이 실행되고, await를 만나 2초 일시정지 되는 동안 coroutine1의 마지막 print -&amp;gt; coroutine2의 마지막 print가 실행되고 종료되게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 여기서 의문이 들 수 있습니다. await를 만나면 일시정지 상태가 되는가? 반은 맞고 반은 틀립니다. await는 일시정지가 될 가능성이 있다는 &lt;code&gt;힌트&lt;/code&gt;일 뿐입니다.&lt;/p&gt;
&lt;h1&gt;Python Frame Object &amp;amp; Byte Code&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후반부에서 다룰 코루틴이 함수를 일시중지하고 재개하는 메커니즘을 이해하기 위해서는 먼저 Frame 객체 그리고 바이트 코드를 알아야 합니다. 먼저 Frame 객체에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Frame Object&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/python-frame-object.png&quot; alt=&quot;frame-object&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://docs.python.org/3/reference/datamodel.html#frame-objects&quot;&gt;Frame&lt;/a&gt; 객체는 함수를 실행하기 위해 필요한 정보들을 담고 있는 객체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접해보는게 이해가 빠르기 때문에 간단한 실습을 통해서 이해해봅시다. inspect 모듈을 import하면 현재 프레임을 얻어올 수 있게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;# frame_example.py
import inspect

frame = None

def func():
    global frame
    x = 10
    y = 20
    print(x + y)
    frame = inspect.currentframe()

func()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 전역 변수 frame에 func 마지막 프레임이 담겼을 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/3/reference/datamodel.html#frame-objects&quot;&gt;Frame&lt;/a&gt; 객체에는 여러 메서드들이 있지만 중점적으로 살펴볼 함수들은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;f_locals&lt;/li&gt;
&lt;li&gt;f_back&lt;/li&gt;
&lt;li&gt;f_lasti&lt;/li&gt;
&lt;li&gt;f_code&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;f_locals&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;print(f&quot;frame.f_locals: {frame.f_locals}&quot;)
&amp;gt;&amp;gt; frame.f_locals: {'x': 10, 'y': 20}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;f_locals는 지역 변수의 상태를 dictionary 형태로 저장하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;f_back&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;print(f&quot;frame.f_back: {frame.f_back}&quot;)
&amp;gt;&amp;gt; frame.f_back: &amp;lt;frame at 0x103045a40, file '/Users/shlee/workspaces/study/iseunghan-Lab/python-deep-dive-into-coroutine/frame_example.py', line 14, code &amp;lt;module&amp;gt;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;f_back은 이전 스택 프레임 즉, 이 프레임을 호출한 caller를 가리킵니다. 이 f_back 정보를 들고 있기 때문에 현재 프레임이 종료되면 f_back을 통해 이전 프레임으로 돌아갈 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/iseunghan/iseunghan-Lab/blob/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/callstack_example.py&quot;&gt;예제 코드&lt;/a&gt;에 대한 Call Stack을 좀 더 이해하기 쉽게 짤로 표현해봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/python_call_stack.gif&quot; alt=&quot;python-call-stack-gif&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;새로운 Call (함수 호출)이 발생하면 Frame이 생기게 되고, f_back에는 caller의 정보가 담기게 됩니다. 그 덕분에 함수가 완전히 종료되면 f_back에 있는 정보를 따라 이전 프레임으로 돌아갈 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;f_lasti&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;print(f&quot;frame.f_lasti: {frame.f_lasti}&quot;)
&amp;gt;&amp;gt; frame.f_lasti: 30&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;f_lasti의 값이 30이 나왔습니다. 이게 무슨 값인지 알기 위해서는 바이트 코드를 까봐야 합니다. 바이트 코드는 &lt;a href=&quot;https://docs.python.org/3/library/dis.html&quot;&gt;dis&lt;/a&gt; 모듈을 import해서 &lt;code&gt;disassemble&lt;/code&gt; 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;# byte_code_example.py
import inspect

def func():
    global frame
    x = 10
    y = 20
    print(x + y)
    frame = inspect.currentframe()

func()

print(f&quot;frame.f_lasti: {frame.f_lasti}&quot;)

import dis
dis.dis(func)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;30
frame.f_lasti: 30
6           0 LOAD_CONST               1 (10)
            2 STORE_FAST               0 (x)

7           4 LOAD_CONST               2 (20)
            6 STORE_FAST               1 (y)

8           8 LOAD_GLOBAL              0 (print)
            10 LOAD_FAST                0 (x)
            12 LOAD_FAST                1 (y)
            14 BINARY_ADD
            16 CALL_FUNCTION            1
            18 POP_TOP

9          20 LOAD_GLOBAL              1 (inspect)
            22 LOAD_METHOD              2 (currentframe)
            24 CALL_METHOD              0
            26 STORE_GLOBAL             3 (frame)
            28 LOAD_CONST               0 (None)
            30 RETURN_VALUE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lasti는 마지막 라인에 있는 RETURN_VALUE의 30을 가리킵니다. 즉, Frame의 가장 최근에 실행된 바이트 코드의 인덱스(offset)를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;byte 코드를 읽는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/3/library/dis.html#python-bytecode-instructions&quot;&gt;공식문서&lt;/a&gt;에 따르면, 각 컬럼에 대해서는 다음과 같이 정의할 수 있습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&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;&amp;nbsp;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;starts_line&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;해당 바이트코드 명령어가 시작되는 소스 코드의 줄 번호. 새로운 줄에서만 표시됨.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;, &lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;offset&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;바이트코드에서 명령어의 위치를 나타내는 오프셋. 보통 2씩 증가함.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, &lt;code&gt;4&lt;/code&gt; 등&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;opname&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;바이트코드 명령어의 이름 (Operation Code Name).&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LOAD_FAST&lt;/code&gt;, &lt;code&gt;CALL&lt;/code&gt;, &lt;code&gt;RETURN_VALUE&lt;/code&gt; 등&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;arg(또는 oparg)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;명령어에 전달되는 인자 값. 특정 명령어에서만 표시됨.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt; 등&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;argval(또는 opargval)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;인자의 실제 값. 예: 변수명, 상수 값 등.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'x'&lt;/code&gt;, &lt;code&gt;'Hello'&lt;/code&gt; 등&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;f_code&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;print(f&quot;frame.f_code: {frame.f_code}&quot;)
&amp;gt;&amp;gt; frame.f_code: &amp;lt;code object func at 0x101bdeb80, file &quot;/Users/shlee/workspaces/study/iseunghan-Lab/python-deep-dive-into-coroutine/frame_example.py&quot;, line 3&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;code&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;f_code는 function의 &lt;code&gt;__code__&lt;/code&gt;와 동일한 객체입니다.&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;frame.f_code is func.__code__
&amp;gt;&amp;gt; True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;code 객체에도 여러 가지 함수가 있지만 중점적으로 살펴볼 함수는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;co_const&lt;/li&gt;
&lt;li&gt;co_names&lt;/li&gt;
&lt;li&gt;co_varnames&lt;/li&gt;
&lt;li&gt;co_code&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 차근차근 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;co_code&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;print(func.__code__.co_code)
&amp;gt;&amp;gt; b'd\x01}\x00d\x02}\x01t\x00|\x00|\x01\x17\x00\x83\x01\x01\x00t\x01\xa0\x02\xa1\x00a\x03d\x00S\x00'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;co_code를 출력해보니 바이트열이 담겨있습니다. 이걸 list로 변환해서 출력해보면?&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;print(list(func.__code__.co_code))
&amp;gt;&amp;gt; [100, 1, 125, 0, 100, 2, 125, 1, 116, 0, 124, 0, 124, 1, 23, 0, 131, 1, 1, 0, 116, 1, 160, 2, 161, 0, 97, 3, 100, 0, 83, 0]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알 수 없는 숫자열이 담겨있습니다. 바로 op_code와 op_arg를 나타냅니다. dis 모듈을 이용해서 func를 바이트 코드로 변환하여 비교해볼까요?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_op_name_op_arg.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;co_code의 숫자값들이 정말 &lt;code&gt;[op_code, op_arg, ...]&lt;/code&gt;를 나타내는지 확인해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;import opcode

opcode.opname[100]
&amp;gt;&amp;gt; LOAD_CONST

opcode.opname[125]
&amp;gt;&amp;gt; STORE_FAST&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dis 모듈로 확인한 바이트 코드의 op_name과 동일한 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해보자면, co_code는 op_code와 op_arg를 순서대로 나열시킨 바이트열이라고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;co_consts&lt;/code&gt;&lt;br /&gt;함수 내부에서 사용중인 상수들을 나타냅니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;print(func.__code__.co_consts)
&amp;gt;&amp;gt; (None, 10, 20)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 None은 함수의 기본 반환 값으로 기존 반환 값 여부 상관없이 항상 None 고정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 co_consts에는 함수의 리턴값 또는 매개변수에 대한 정보는 포함되지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# frame_f_code_example.py
print(f&quot;func.co_consts: {func.__code__.co_consts}&quot;)
# &amp;gt;&amp;gt; func.co_consts: (None, 10, 20)

def func2(arg2=&quot;world&quot;) -&amp;gt; str:
    return f&quot;Hello, {arg2}&quot;

print(f&quot;func2.co_consts: {func2.__code__.co_consts}&quot;)
# &amp;gt;&amp;gt; func2.co_consts: (None, 'Hello, ')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;co_varnames&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;# frame_f_code_example.py
func.__code__.co_varnames
# &amp;gt;&amp;gt; ('x', 'y')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 내의 지역변수명을 튜플 형태로 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;co_names&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;func.__code__.co_names
# &amp;gt;&amp;gt; ('print', 'inspect', 'currentframe', 'frame')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 내의 전역변수명을 튜플의 형태로 저장합니다.&lt;br /&gt;print, inspect 등의 함수들은 built-in 함수라서 전역변수 취급 되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;byte 코드, frame을 함께 살펴봅시다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 바이트 코드가 어떻게 동작하는지 지금부터 Step-by-Step으로 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;(저도 이해가 잘 가지 않아서 직접 그려가면서 따라가보았습니다.)&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이트 코드는 다음과 같이 예제 코드를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;# byte_code_with_frame_ex.py
def func():
    x = 10
    y = 20
    print(x + y)

import dis
dis.dis(func)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2           0 LOAD_CONST               1 (10)
            2 STORE_FAST               0 (x)

3           4 LOAD_CONST               2 (20)
            6 STORE_FAST               1 (y)

4           8 LOAD_GLOBAL              0 (print)
            10 LOAD_FAST                0 (x)
            12 LOAD_FAST                1 (y)
            14 BINARY_ADD
            16 CALL_FUNCTION            1
            18 POP_TOP
            20 LOAD_CONST               0 (None)
            22 RETURN_VALUE&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;먼저 LOAD_CONST입니다. op_arg(1)는 LOAD_CONST를 보시면 co_const를 가리킨다는 것을 쉽게 이해하실 수 있습니다. 인덱스 1의 value 10을 취해서 value_stack에 밀어넣습니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;STORE_FAST는 현재 Value_stack에 있는 상단 값을 뽑아서 op_arg(0)에 저장시킵니다. 여기서 or_arg는 co_varnames를 참조합니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-3.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;1번과 마찬가지로 op_arg(2)를 co_const에서 가져와서 value_stack에 밀어넣습니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-4.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;2번과 마찬가지로, 현재 Value_stack에 있는 상단 값을 뽑아서 op_arg(1) 즉, y에 20을 저장시킵니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-5.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;LOAD_GLOBAL은 co_names 즉, 전역변수를 로드하는 작업입니다. op_arg(0) -&amp;gt; print를 value_stack에 올립니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-6.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;10, 12번은 동일한 LOAD_FAST이므로 f_local에 있는 인덱스 0번과 1번 즉, &lt;code&gt;x=10, y=20&lt;/code&gt;을 value_stack에 올립니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-7.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;10과 20을 pop한 뒤 BINARY_ADD를 수행한 결과인 30을 다시 넣습니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-8.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;CALL_FUNCTION을 수행합니다. op_arg(1)의 의미는 함수를 호출하는데 사용하는 인자를 value_stack에서 1개를 사용하겠다는 것 입니다. 즉 print 함수에 30이 전달되어 실행됩니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-9.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;print 함수의 명시적 반환값이 없으므로 기본적으로 None을 리턴하게 됩니다. 사용하지 않는 값이므로 POP_TOP을 수행하여 제거해줍니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-10.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;co_const의 0번 인덱스(None)를 LOAD_CONST를 수행합니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-11.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;현재 함수 func도 마찬가지로 반환값이 없으므로 None을 RETURN_VALUE 수행하고 종료됩니다.&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/iseunghan/iseunghan-Lab/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/images/byte_code_step_by_step/image-12.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;coroutine 다시 살펴보기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터 실습은 python 3.12로 진행 하였습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 살펴볼 내용들을 이해하기 위한 바이트 코드를 이제는 읽을 수 있게 되었습니다. native coroutine 함수를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# coroutine.py
import asyncio

async def coroutine1():
    print(&quot;coro1 first entry point&quot;)
    await asyncio.sleep(1)
    print(&quot;coro1 second entry point&quot;)

import dis
dis.dis(coroutine1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 위 native coroutine 함수의 바이트 코드를 출력한 것 입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;              4           0 RETURN_GENERATOR
              2 POP_TOP
              4 RESUME                   0

  5           6 LOAD_GLOBAL              1 (NULL + print)
             16 LOAD_CONST               1 ('coro1 first entry point')
             ...
        &amp;gt;&amp;gt;   72 SEND                     3 (to 82)
             76 YIELD_VALUE              2
             78 RESUME                   3
             80 JUMP_BACKWARD_NO_INTERRUPT     5 (to 72)
        &amp;gt;&amp;gt;   82 END_SEND
             84 POP_TOP
            ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈 여겨볼 부분은 await를 하는 부분의 바이트 코드가 YIELD로 해석된다는 점입니다. 이로써 내부적으로는 제네레이터 기반으로 동작한다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Generator 기반 Coroutine 함수도 살펴볼까요?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;generator 기반 코루틴은 python 3.12 이후 버전부터 지원하지 않습니다. (ref. &lt;a href=&quot;https://github.com/python/typeshed/issues/10116&quot;&gt;https://github.com/python/typeshed/issues/10116&lt;/a&gt;), 아래 예제가 정확하지 않을 수 있습니다  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# generator-coroutine.py
def coroutine3():
    print(&quot;coroutine3 first entry point&quot;)
    yield from asyncio.sleep(1)
    print(&quot;coroutine3 second entry point&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yield와 yield from 차이점은?&lt;br /&gt;yield from은 Generator 내부에서 또 다른 sub Generator를 실행하기 위해 사용합니다. 실행권한을 sub Generator에게 위임하고 Return 결과를 받을 수 있게 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이트 코드로 출력해서 살펴보면,&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;8           0 RETURN_GENERATOR
              2 POP_TOP
              4 RESUME                   0

  9           6 LOAD_GLOBAL              1 (NULL + print)
             16 LOAD_CONST               1 ('coroutine3 first entry point')
             18 CALL                     1
             26 POP_TOP

 10          28 LOAD_GLOBAL              3 (NULL + asyncio)
             38 LOAD_ATTR                4 (sleep)
             58 LOAD_CONST               2 (1)
             60 CALL                     1
             68 GET_YIELD_FROM_ITER
             70 LOAD_CONST               0 (None)
        &amp;gt;&amp;gt;   72 SEND                     3 (to 82)
             76 YIELD_VALUE              2
             78 RESUME                   2
             80 JUMP_BACKWARD_NO_INTERRUPT     5 (to 72)
        &amp;gt;&amp;gt;   82 END_SEND
             84 POP_TOP
            ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;yield from asyncio.sleep(1)&lt;/code&gt; 함수는 다음과 같이 해석되고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;10          28 LOAD_GLOBAL              3 (NULL + asyncio)
             38 LOAD_ATTR                4 (sleep)
             58 LOAD_CONST               2 (1)
             60 CALL                     1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asyncio를 로드해서 sleep 함수 그리고 매개변수 1을 넘겨 call 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;             68 GET_YIELD_FROM_ITER
             70 LOAD_CONST               0 (None)
        &amp;gt;&amp;gt;   72 SEND                     3 (to 82)
             76 YIELD_VALUE              2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 다음, GET_YIELD_FROM_ITER를 통해 awaitable을 iterator로 변환 (&lt;b&gt;await&lt;/b&gt;())하고, Send를 통해 중첩된 서브 제네레이터를 실행한 다음 YIELD를 통해 값을 밖으로 밀어넣고 일시중지 시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 yield를 통해 일시정지되고 다시 재개할 수 있는걸까요? Generator 함수를 살펴봅시다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# generator.py
def generator():
    recv = yield 1
    return recv

import dis
dis.dis(generator)

gen = generator()
print(gen.send(None)) # 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;byte code는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;  1           0 RETURN_GENERATOR
              2 POP_TOP
              4 RESUME                   0

  2           6 LOAD_CONST               1 (1)
              8 YIELD_VALUE              1
             10 RESUME                   1
             12 STORE_FAST               0 (recv)

  3          14 LOAD_FAST                0 (recv)
             16 RETURN_VALUE
        &amp;gt;&amp;gt;   18 CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)
             20 RERAISE                  1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;gen.send(None)&lt;/code&gt;를 실행하면 yield까지 실행되고 1이 send 호출자에게 전달되고 suspend 되게 됩니다. (이것은 제네레이터 동작방식을 이미 알고 있다면 이해하고 계실겁니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 yield 부분에서 일시정지 되었는지 Generator 함수의 Frame 객체를 얻어와서 확인해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;# generator.py
lasti = gen.gi_frame.f_lasti
print(f&quot;&amp;gt;&amp;gt; f_lasti: {lasti}&quot;)

code = gen.gi_code.co_code
op = code[lasti]

import opcode
print(f&quot;&amp;gt;&amp;gt; op: {op}, opname: {opcode.opname[op]}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt; f_lasti: 8
&amp;gt;&amp;gt; op: 150, opname: YIELD_VALUE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;frame의 최근의 실행된 바이트코드의 인덱스는 8으로 확인되었고, code를 얻어와서 opname을 확인해보니 YIELD_VALUE인 것이 확인되었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;send를 더 자세하게 살펴보기 위해 CPython 인터프리터 내부의 실제 send를 수행하는 함수를 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네레이터를 더 자세하게 살펴보고 싶으시면, 다음 &lt;a href=&quot;https://github.com/iseunghan/iseunghan-Lab/blob/a48d36b3e3b28c7c61071c3375ed5f560baa7e5b/python-deep-dive-into-coroutine/Generator.md&quot;&gt;Generator.md&lt;/a&gt;를 참조해주세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPython 내부 살펴보기 (gen_send)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gen.send()를 수행하면 실제로는 &lt;a href=&quot;https://github.com/python/cpython/blob/3d396ab7591d544ac8bc1fb49615b4e867ca1c83/Objects/genobject.c#L298&quot;&gt;genobject.send_ex&lt;/a&gt;함수가 실행됩니다. 내부에서는 다시 &lt;a href=&quot;https://github.com/python/cpython/blob/3d396ab7591d544ac8bc1fb49615b4e867ca1c83/Objects/genobject.c#L192&quot;&gt;genobject.send_ex_2&lt;/a&gt;함수를 호출하게 되는데요. 간단하게 살펴보면, gen_send_ex2를 수행해서 PYGEN_RETURN 상태가 된다면 조건문 내부를 수행하는 것 같습니다. gen_send_ex2에 넘겨준 result에 결과에 따라서 Generator를 StopIteration할지 결정하는 것 같습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;static PyObject *
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
{
    PyObject *result;
    if (gen_send_ex2(gen, arg, &amp;amp;result, exc, closing) == PYGEN_RETURN) {
        if (PyAsyncGen_CheckExact(gen)) {
            assert(result == Py_None);
            PyErr_SetNone(PyExc_StopAsyncIteration);
        }
        else if (result == Py_None) {
            PyErr_SetNone(PyExc_StopIteration);
        }
        else {
            _PyGen_SetStopIterationValue(result);
        }
        Py_CLEAR(result);
    }
    return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 &lt;a href=&quot;https://github.com/python/cpython/blob/3d396ab7591d544ac8bc1fb49615b4e867ca1c83/Objects/genobject.c#L192&quot;&gt;genobject.send_ex_2&lt;/a&gt;함수입니다. 너무 많아서 일부분만 발췌했습니다.&lt;br /&gt;먼저 Thread로부터 ThreadState를 가져오고, 인자로 받은 generator의 &lt;code&gt;Frame&lt;/code&gt; 객체를 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 send로 넘어온 arg를 삼항 연산자로 None 처리를 해준 다음 &lt;code&gt;_PyFrame_StackPush&lt;/code&gt; 함수에 현재 Frame과 arg를 넘겨 Frame의 Value_Stack에 &lt;code&gt;Push&lt;/code&gt;해줍니다. (이제 어떻게 generator yield 자리에 값이 치환 되는지 알게 되었습니다)&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;static PySendResult
gen_send_ex2(PyGenObject *gen, PyObject *arg, PyObject **presult,
             int exc, int closing)
{
    PyThreadState *tstate = _PyThreadState_GET();
    _PyInterpreterFrame *frame = &amp;amp;gen-&amp;gt;gi_iframe;

    ...

    /* Push arg onto the frame's value stack */
    PyObject *arg_obj = arg ? arg : Py_None;
    _PyFrame_StackPush(frame, PyStackRef_FromPyObjectNew(arg_obj));

    ...

    gen-&amp;gt;gi_frame_state = FRAME_EXECUTING;
    EVAL_CALL_STAT_INC(EVAL_CALL_GENERATOR);
    PyObject *result = _PyEval_EvalFrame(tstate, frame, exc);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음으로는 현재 Frame을 실행상태로 변경하고, &lt;code&gt;_PyEval_EvalFrame&lt;/code&gt; 함수를 수행합니다. 처음에는 _PyEval_EvalFrame의 구현체를 찾지 못했는데, 공식문서를 뒤지던 도중 &lt;a href=&quot;https://github.com/python/cpython/blob/3d396ab7591d544ac8bc1fb49615b4e867ca1c83/InternalDocs/compiler.md?plain=1#L448&quot;&gt;compiler.md&lt;/a&gt;에서 ceval.h를 참조하라는 코멘트 덕분에 실제 &lt;a href=&quot;https://github.com/python/cpython/blob/3d396ab7591d544ac8bc1fb49615b4e867ca1c83/Python/ceval.c#L1009&quot;&gt;_PyEval_EvalFrame&lt;/a&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;PyObject *
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
    ...
    for (;;) {
        // instruction fetch
        NEXTOPARG();
        // dispatch opcode
        switch (opcode) {
            case LOAD_FAST:
                ...
                DISPATCH();
            case YIELD_VALUE:
                retval = TOP();
                STACK_SHRINK(1);
                f-&amp;gt;f_state = FRAME_SUSPENDED;
                return retval;
            ...
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 내용을 정리해보자면, &lt;code&gt;Frame&lt;/code&gt; 객체는 함수가 실행될 때 필요한 정보(Value Stack, Local Variable 등)들을 담고 있는 객체입니다. f_back을 통해 Call Stack을 만들 수 있고, f_lasti(Last attemped bytecode)를 통해 함수를 일시정지 및 재개를 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Coroutine은 Generator 기반으로 동작하는 것을 확인하였고, Thread 처럼 Frame 객체를 가지고 있는 것을 알게 되었습니다. (&lt;code&gt;PyThreadState *tstate = _PyThreadState_GET()&lt;/code&gt;)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;asyncio.sleep 내부 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/tasks.py#L653&quot;&gt;asyncio.sleep()&lt;/a&gt; 함수 내부는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;async def sleep(delay, result=None):
    &quot;&quot;&quot;Coroutine that completes after a given time (in seconds).&quot;&quot;&quot;
    if delay &amp;lt;= 0:
        await __sleep0()
        return result
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 delay가 0이하라면 단순히 private 메서드인 &lt;code&gt;__sleep0&lt;/code&gt;을 실행시키는데요 &lt;code&gt;__sleep0&lt;/code&gt; 메서드를 살펴보면 단순히 &lt;code&gt;yield&lt;/code&gt;만 수행합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;@types.coroutine
def __sleep0():
    &quot;&quot;&quot;Skip one event loop run cycle.

    This is a private helper for 'asyncio.sleep()', used
    when the 'delay' is set to 0.  It uses a bare 'yield'
    expression (which Task.__step knows how to handle)
    instead of creating a Future object.
    &quot;&quot;&quot;
    yield&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 아무 의미 없는 것처럼 보이지만, 현재 이벤트 루프의 선점을 포기하여 다음 스케줄링된 Task에게 우선권을 넘기는 작업을 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sleep 함수의 delay가 0보다 클 때, 로직을 살펴보면, future 객체를 생성하고 event loop에게 delay 이후에 &lt;code&gt;futures._set_result_unless_cancelled&lt;/code&gt; 메서드를 호출해달라고 등록합니다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;async def sleep(delay, result=None):
    if delay &amp;lt;= 0:
        await __sleep0()
        return result

    loop = events.get_running_loop()
    future = loop.create_future()
    h = loop.call_later(delay,
                        futures._set_result_unless_cancelled,
                        future, result)
    try:
        return await future&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막에 future를 await를 수행하게 되는데, await는 실제로 yield from 즉, Generator의 &lt;code&gt;send&lt;/code&gt;를 수행한다는 사실을 우린 이미 알고있습니다. 결론적으론 해당 객체의 &lt;code&gt;__iter__&lt;/code&gt; 또는 &lt;code&gt;__await__&lt;/code&gt; 구현체를 찾아 실행시키게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 future 객체를 생성했으니까 &lt;code&gt;Future&lt;/code&gt; 클래스의 &lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/futures.py#L286&quot;&gt;&lt;code&gt;__await__&lt;/code&gt;&lt;/a&gt; 함수를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class Future:
    def __await__(self):
        if not self.done():
            self._asyncio_future_blocking = True
            yield self  # This tells Task to wait for completion.
        if not self.done():
            raise RuntimeError(&quot;await wasn't used with future&quot;)
        return self.result()  # May raise too.

    __iter__ = __await__  # make compatible with 'yield from'.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기자신인 self를 yield하는 것을 확인할 수 있는데요, 이렇게 yield된 값을 받는 곳은 바로 Task 객체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task 클래스의 &lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/tasks.py#L291&quot;&gt;_step&lt;/a&gt; 함수를 살펴보겠습니다.&lt;br /&gt;이벤트 루프에는 바로 &lt;code&gt;_step&lt;/code&gt; 메서드가 스케줄링 된 것입니다. 다시 정리하면, 코루틴이 이벤트 루프에 스케줄링 된다는 말은 Task 객체의 _step 메서드가 이벤트 루프에 스케줄링 된다고 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class Task(Future):
    def __step(self, exc=None):
        ...
        try:
            self.__step_run_and_handle_result(exc)
        finally:
            _leave_task(self._loop, self)
            self = None  # Needed to break cycles when an exception occurs.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;__step_run_and_handle_result&lt;/code&gt; 메서드를 살펴보면, coroutine 객체를 가져와서 None을 Send 합니다. 그렇게 result를 받아오는데 이때 result는 Future 인스턴스입니다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;class Task(Future):
    def __step_run_and_handle_result(self, exc):
        coro = self._coro
        try:
            if exc is None:
                result = coro.send(None)
            else:
                result = coro.throw(exc)
        except StopIteration as exc:
            if self._must_cancel:
                self._must_cancel = False
                super().cancel(msg=self._cancel_message)
            else:
                super().set_result(exc.value)
        except exceptions.CancelledError as exc:
            self._cancelled_exc = exc
            super().cancel()
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음, result 인스턴스가 실제로 awaitable 한 객체인지 &lt;code&gt;_asyncio_future_blocking&lt;/code&gt; 속성(기본 값: None)을 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;result 인스턴스 즉, Future 인스턴스가 awaitable 한 객체라면, &lt;code&gt;__wakeup&lt;/code&gt; 함수를 &lt;code&gt;add_done_callback&lt;/code&gt; 함수에게 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 추가적으로 현재 Future가 다른 event loop에 attached 되었는지, 자기 자신을 await 하는지 등 검증 로직도 포함되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;class Task(Future):
    def __step_run_and_handle_result(self, exc):
        ...
        except exceptions.CancelledError as exc:
            ...
        else:
            blocking = getattr(result, '_asyncio_future_blocking', None)
            if blocking is not None:
                if futures._get_loop(result) is not self._loop:
                    ...
                elif blocking:
                    if result is self:
                        new_exc = RuntimeError(
                            f'Task cannot await on itself: {self!r}')
                        self._loop.call_soon(
                            self.__step, new_exc, context=self._context)
                    else:
                        result._asyncio_future_blocking = False
                        result.add_done_callback(
                            self.__wakeup, context=self._context)
                        self._fut_waiter = result
                        if self._must_cancel:
                            if self._fut_waiter.cancel(
                                    msg=self._cancel_message):
                                self._must_cancel = False
                ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 &lt;a href=&quot;https://github.com/python/cpython/blob/49d72365cd2d6c09a154a9a061efef4130e2c758/Lib/asyncio/futures.py#L226&quot;&gt;&lt;code&gt;Future.add_done_callback&lt;/code&gt;&lt;/a&gt; 함수는 어떤 것을 수행하는지 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로는 단순하게 인자로 받은 &lt;code&gt;fn&lt;/code&gt;을 내부 &lt;code&gt;_callbacks&lt;/code&gt; 리스트에 append를 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;def add_done_callback(self, fn, *, context=None):
    ...
    self._callbacks.append((fn, context))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 Future가 완료됐을 때 실행되는 &lt;a href=&quot;https://github.com/python/cpython/blob/49d72365cd2d6c09a154a9a061efef4130e2c758/Lib/asyncio/futures.py#L257C5-L267C36&quot;&gt;Future.set_result&lt;/a&gt; 함수에서는 Future의 상태를 FINISHED로 마킹하고 또 다시 &lt;code&gt;__schedule_callbacks&lt;/code&gt;를 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def set_result(self, result):
    ...
    self._result = result
    self._state = _FINISHED
    self.__schedule_callbacks()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/python/cpython/blob/49d72365cd2d6c09a154a9a061efef4130e2c758/Lib/asyncio/futures.py#L167&quot;&gt;Future.__schedule_callbacks&lt;/a&gt; 에서는 작업을 끝내는게 아닌, 또 다시 call_soon을 호출하게 됩니다. 직접 실행하는게 아닌 call_soon에게 처리를 위임시킵니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def __schedule_callbacks(self):
    callbacks = self._callbacks[:]
    if not callbacks:
        return

    self._callbacks[:] = []
    for callback, ctx in callbacks:
        self._loop.call_soon(callback, self, context=ctx)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 &lt;code&gt;__wakeup&lt;/code&gt; 함수입니다. &lt;b&gt;wakeup 내부적으로 다시 Task.&lt;/b&gt;step()을 호출하여 현재 Task를 재개하는 역할을 수행합니다. await 된 Future 객체가 완료될 때까지 &lt;code&gt;__step -&amp;gt; __wakeup -&amp;gt; __step ...&lt;/code&gt; 이런 과정들이 반복되면서 스케줄링 되고 있던 것을 알 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;def __wakeup(self, future):
        try:
            future.result()
        except BaseException as exc:
            # This may also be a cancellation.
            self.__step(exc)
        else:
            # Don't pass the value of `future.result()` explicitly,
            # as `Future.__iter__` and `Future.__await__` don't need it.
            # If we call `_step(value, None)` instead of `_step()`,
            # Python eval loop would use `.send(value)` method call,
            # instead of `__next__()`, which is slower for futures
            # that return non-generator iterators from their `__iter__`.
            self.__step()
        self = None  # Needed to break cycles when an exception occurs.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 코루틴 예제(coroutine.py)를 살펴보면, create_task 부분이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;async def coroutine1():
    print(&quot;coro1 first entry point&quot;)
    await asyncio.sleep(1)
    print(&quot;coro1 second entry point&quot;)

async def coroutine2():
    print(&quot;coro2 first entry point&quot;)
    await asyncio.sleep(1)
    print(&quot;coro2 second entry point&quot;)

loop = asyncio.get_event_loop()
loop.create_task(coroutine1())
loop.create_task(coroutine2())
loop.run_forever()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create_task를 수행하게 되면 실제로는 &lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/tasks.py#L139&quot;&gt;Task의 &lt;code&gt;__init__()&lt;/code&gt;&lt;/a&gt; 가 호출되게 되는데요.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;class Task(futures._PyFuture):
    def __init__(self, coro, *, loop=None, name=None, context=None,
                 eager_start=False):
        ...
            self._loop.call_soon(self.__step, context=self._context)
            _register_task(self)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 _step 함수를 event loop에 call_soon 함수를 이용해서 스케줄링하는 것을 확인하실 수 있습니다!&lt;/p&gt;
&lt;h1&gt;Custom Event Loop를 만들어보기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 Custom Event Loop를 직접 만들어 보겠습니다. 직접 만들어보면서 내부적으로 서로 유기적으로 연결되어있는지 또 어떻게 동작하는지 더욱 더 깊게 파악할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 EventLoop를 구현하기 전에 내부적으로 다루는 Handle 객체와 TimeHandle 객체를 이해해야 합니다.&lt;br /&gt;&lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/events.py#L29&quot;&gt;Handle&lt;/a&gt; 객체는 콜백함수를 래핑한 함수입니다. 초기화를 할 때, 콜백함수와 매개변수를 받은 다음 _run을 통해서 해당 콜백함수를 실행하는 역할을 합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class Handle:
    def __init__(self, callback, args, loop, context=None):
        if context is None:
            context = contextvars.copy_context()
        self._context = context
        self._loop = loop
        self._callback = callback
        self._args = args
        ...

    def _run(self):
        try:
            self._context.run(self._callback, *self._args)
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 &lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/events.py#L106&quot;&gt;TimeHandle&lt;/a&gt; 객체입니다. Handle을 상속받았는데 when이라는 속성을 더 가졌습니다. when 속성은 Handle 객체가 언제 실행되어야 하는지에 대한 시간 정보를 나타내기 때문에 EventLoop에서 해당 when을 보고 실행시켜야 하는지를 판단합니다. 그리고 TimerHandle에는 when을 비교하는 여러 메서드(lt, le, gt, ge, eq 등)가 구현되어있으므로, 나중에 시간순으로 나열할 때 유용합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class TimerHandle(Handle):
    def __init__(self, when, callback, args, loop, context=None):
        super().__init__(callback, args, loop, context)
        if self._source_traceback:
            del self._source_traceback[-1]
        self._when = when
        self._scheduled = False

    def __lt__(self, other):
        ...

    def __gt__(self, other):
        if isinstance(other, TimerHandle):
            return self._when &amp;gt; other._when

    def __eq__(self, other):
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asyncio 패키지 내부에 &lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/events.py#L211&quot;&gt;AbstractEventLoop&lt;/a&gt;라는 클래스가 있는데 이 클래스를 상속받아 구현하면 이벤트 루프를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자에서는 특정 시점에 실행되어야 할 &lt;code&gt;TimeHandle&lt;/code&gt; 객체를 저장할 scheduled 리스트 변수와 곧 실행될 Handle 객체를 저장할 ready deque를 들고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class CustomEventLoop(AbstractEventLoop):
    def __init__(self):
        self._scheduled = []
        self._ready = deque()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 앞서 살펴본 &lt;code&gt;call_soon&lt;/code&gt; 함수 입니다. &lt;code&gt;call_soon&lt;/code&gt; 함수는 곧 실행될 Handle 객체를 생성하여 _ready deque에 추가합니다. &lt;code&gt;call_later&lt;/code&gt; 함수는 특정 시간에 실행될 TimeHandle 객체를 생성하는데요, 현재 시간 + delay를 when 매개변수로 &lt;code&gt;call_at&lt;/code&gt; 함수를 호출하는데요, &lt;code&gt;call_at&lt;/code&gt; 함수는 when을 가지고 TimeHandle 객체를 생성하여 scheduled에 추가해줌으로써 스케줄링이 시작됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class CustomEventLoop(AbstractEventLoop):
    def call_soon(self, callback, *args, context=None):
        handle = Handle(callback, args, self, context)
        self._ready.append(handle)
        return handle

    def call_later(self, delay, callback, *args):
        timer = self.call_at(self.time() + delay, callback, *args)
        return timer

    def call_at(self, when, callback, *args):
        timer = TimerHandle(when, callback, args, self)
        heappush(self._scheduled, timer)
        return timer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create_future와 create_task는 내부적으로 각각 Future, Task를 생성하여 반환합니다. time 함수는 현재 시간의 초를 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class CustomEventLoop(AbstractEventLoop):
    def create_future(self):
        return Future(loop=self)

    def create_task(self, coro):
        return Task(coro, loop=self)

    def time(self):
        return time.monotonic()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 get_debug, _timer_handle_cancelled, call_exception_handler 함수들은 사용하지 않지만 구현하지 않으면 NotImplementedError가 발생하기 때문에 아래와 같이 구현만 해줬습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class CustomEventLoop(AbstractEventLoop):
    def get_debug(self):
        pass

    def _timer_handle_cancelled(self, handle):
        pass

    def call_exception_handler(self, context):
        pass&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 &lt;code&gt;run_forever&lt;/code&gt; 함수입니다. 함수 내부에서는 while문 무한루프를 돌면서 &lt;code&gt;_run_once&lt;/code&gt; 함수를 지속적으로 콜합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class CustomEventLoop(AbstractEventLoop):
    def run_forever(self):
        while True:
            self._run_once()

    def _run_once(self):
        while self._scheduled and self._scheduled[0]._when &amp;lt;= self.time():
            timer: TimerHandle = heappop(self._scheduled)
            self._ready.append(timer)

        len_ready = len(self._ready)
        for _ in range(len_ready):
            handle: TimerHandle = self._ready.popleft()
            handle._run() # 내부적으로 Task의 _step()도 함께 호출된다.

        timeout = 0
        if self._scheduled and not self._ready:
            timeout = max(0, self._scheduled[0]._when - self.time())
        time.sleep(timeout) # &amp;lt;- 무한루프에 빠질 위험이 있다
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;_run_once&lt;/code&gt; 함수를 살펴보면 다음과 같은 순서로 실행됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스케줄링 된 TimeHandle이 있다면, 현재 시각 기준으로 실행되어야 할 작업인지 확인합니다.&lt;/li&gt;
&lt;li&gt;heap에서 _when이 가장 빠른 작업을 pop하여 가져온 뒤, _ready 대기열에 추가해줍니다.&lt;/li&gt;
&lt;li&gt;_ready를 순회하면서, &lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/events.py#L86&quot;&gt;&lt;code&gt;Handle._run&lt;/code&gt;&lt;/a&gt; 함수를 실행합니다. &lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/events.py#L86&quot;&gt;_run&lt;/a&gt; 함수를 살펴볼까요?멤버변수인 _context.run을 호출하고 있습니다. _context는 Handle 객체가 생성될 때, Task들이 독립적으로 실행될 수 있도록 &lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/events.py#L38&quot;&gt;기본적으로 contextvars.Context 객체를 복사해서 사용&lt;/a&gt;하고 있습니다. 결국 독립된 컨텍스트에서 _callback 함수를 실행하는 역할을 수행합니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-python&quot;&gt;def _run(self):
 try:
     self._context.run(self._callback, *self._args)
 ...
 self = None  # Needed to break cycles when an exception occurs.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;여기서부터는 &lt;code&gt;[asyncio.sleep 내부 살펴보기]&lt;/code&gt;에서 봤던 Task 클래스의 &lt;a href=&quot;https://github.com/python/cpython/blob/fd6c5fe7869fd0519f2a222e769553b91815ff1a/Lib/asyncio/tasks.py#L291&quot;&gt;_step&lt;/a&gt; 함수부터 이벤트 루프에 의해 스케줄링됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CustomEventLoop를 실행해봅시다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomEventLoop를 통해서 코루틴을 실행하는 간단한 실습을 해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# custom_event_loop_run.py
import asyncio
from custom_event_loop import CustomEventLoop


async def coroutine1():
    print(&quot;coro1 first entry point&quot;)
    await asyncio.sleep(1)
    print(&quot;coro1 second entry point&quot;)

async def coroutine2():
    print(&quot;coro2 first entry point&quot;)
    await asyncio.sleep(2)
    print(&quot;coro2 second entry point&quot;)

loop = CustomEventLoop()
asyncio.set_event_loop(loop)

loop.create_task(coroutine1())
loop.create_task(coroutine2())
loop.run_forever()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과:&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python custom_event_loop_eun.py
coro1 first entry point
coro2 first entry point
^CTraceback (most recent call last):
  File &quot;/Users/shlee/workspaces/study/iseunghan-Lab/python-deep-dive-into-coroutine/custom_event_loop_eun.py&quot;, line 21, in &amp;lt;module&amp;gt;
    loop.run_forever()
  File &quot;/Users/shlee/workspaces/study/iseunghan-Lab/python-deep-dive-into-coroutine/custom_event_loop.py&quot;, line 46, in run_forever
    self._run_once()
  File &quot;/Users/shlee/workspaces/study/iseunghan-Lab/python-deep-dive-into-coroutine/custom_event_loop.py&quot;, line 61, in _run_once
    time.sleep(timeout) # &amp;lt;- 무한루프에 빠질 위험이 있다
    ^^^^^^^^^^^^^^^^^^^&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과를 살펴보면, first entry point만 출력되고 second entry point가 출력되지 않았습니다. 왜 이런걸까요? 다음과 같이 디버깅 출력문을 통해 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;class CustomEventLoop(AbstractEventLoop):
    def call_later(self, delay, callback, *args, context=None):
        print(f&quot;[log] call_later: at={self.time() + delay}, delay={delay}, callback={callback}, args={args}, context={context}&quot;)
        return self.call_at(self.time() + delay, callback, *args, context=context)

    def _run_once(self):
        ...
        for _ in range(len_ready):
            handle: TimerHandle = self._ready.popleft()
            print(f&quot;[log] handle: {handle}, when: {handle._run}&quot;)
            handle._run() # 내부적으로 Task의 _step()도 함께 호출된다.

        print(f&quot;[log] Scheduled tasks: {self._scheduled}&quot;)
        print(f&quot;[log] Ready tasks: {self._ready}&quot;)        
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과:&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;[log] call_later: at=43708.48690775, delay=0, callback=&amp;lt;_asyncio.TaskStepMethWrapper object at 0x100647010&amp;gt;, args=(), context=&amp;lt;_contextvars.Context object at 0x10120bbc0&amp;gt;
[log] call_later: at=43708.486941375, delay=0, callback=&amp;lt;_asyncio.TaskStepMethWrapper object at 0x1011b1e70&amp;gt;, args=(), context=&amp;lt;_contextvars.Context object at 0x101209640&amp;gt;

[log] handle: &amp;lt;TimerHandle when=43708.486933416 &amp;lt;_asyncio.TaskStepMethWrapper object at 0x100647010&amp;gt;()&amp;gt;, when: &amp;lt;bound method Handle._run of &amp;lt;TimerHandle when=43708.486933416 &amp;lt;_asyncio.TaskStepMethWrapper object at 0x100647010&amp;gt;()&amp;gt;&amp;gt;
[log] handle: &amp;lt;TimerHandle when=43708.48694775 &amp;lt;_asyncio.TaskStepMethWrapper object at 0x1011b1e70&amp;gt;()&amp;gt;, when: &amp;lt;bound method Handle._run of &amp;lt;TimerHandle when=43708.48694775 &amp;lt;_asyncio.TaskStepMethWrapper object at 0x1011b1e70&amp;gt;()&amp;gt;&amp;gt;

[log] Scheduled tasks: []
[log] Ready tasks: deque([])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 살펴보면, 최초 코루틴을 등록했을 때는 call_later가 호출되어 정상적으로 _scheduled에 등록되었습니다. 이후 Handle 객체로 뽑아와서 _run을 호출하는 로그까지 잘 찍혔지만, 문제는 asyncio.sleep을 만나서 다시 call_later를 통해 이벤트 루프에 등록되어야 하는데 call_later 호출 로그가 찍히지 않은 것을 보니 이벤트 루프를 찾지 못한 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 안될까를 살펴보던 중.. 구글 검색을 통해 관련 &lt;a href=&quot;https://github.com/python/cpython/issues/85134#issuecomment-1264746033&quot;&gt;issue&lt;/a&gt;를 발견했습니다. 이슈를 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신의 이벤트 루프를 구현하려면 (asyncio.baseeventLoop.run_forever ()를 호출하지 않고)를 asyncio._set_running_loop()를 사용해야합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그런지 BaseEventLoop의 &lt;a href=&quot;https://github.com/python/asyncio/pull/452/files#diff-f57505d1f4330e1cb061ee8e5beb4ea518c7f27a9cc6a2ebfea1e28543e2dd46R407&quot;&gt;run_forever&lt;/a&gt; 함수를 보면, &lt;code&gt;_run_once&lt;/code&gt;를 호출하기 전에 running_loop를 자기 자신(BaseEventLoop)으로 등록을 시킵니다. CustomEventLoop를 구현하면서 해당 부분이 누락되었기 때문에 EventLoop가 None을 반환하여 코루틴이 정상적으로 동작하지 못한 것으로 판단됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomEventLoop의 &lt;code&gt;run_forever&lt;/code&gt;에 다음 코드를 추가함으로써 해결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def run_forever(self):
    asyncio._set_running_loop(self)
    while True:
        self._run_once()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 custom_event_loop_run.py를 실행해보면? 정상적으로 실행되는 것을 확인하실 수 있습니다!&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;coro1 first entry point
coro2 first entry point
coro1 second entry point
coro2 second entry point
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;마무리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 generator와 asyncio에 대해서 제대로 학습하지 않은 채로 사용하니 이벤트 루프가 &lt;code&gt;비선점 멀티태스킹&lt;/code&gt;이므로 CPU Bound 작업을 Task로 등록하면 이벤트 루프를 계속 선점하여 다른 Task들이 실행되지 못하는 등등 잘못알고 사용하면 많은 문제점들이 발생합니다. 이번 시간을 통해 이벤트 루프를 실행하는데 필요한 개념들(Generator, CallStack, Frame)을 바이트 코드 레벨까지 살펴봄으로 써 더욱 더 동작원리를 파악하고 적합한 곳에 사용할 수 있을 것 같다고 생각합니다. 감사합니다.&lt;/p&gt;
&lt;h1&gt;REFERENCES&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/NmSeLspQoAA?feature=shared&quot;&gt;Deep Dive into Coroutine - 김대희&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/reference/datamodel.html#frame-objects&quot;&gt;Frame objects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/python/cpython/tree/fd6c5fe7869fd0519f2a222e769553b91815ff1a&quot;&gt;Github Cpython&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  Python</category>
      <category>asyncio</category>
      <category>Coroutine</category>
      <category>EventLoop</category>
      <category>frame</category>
      <category>Generator</category>
      <category>Python</category>
      <category>value stack</category>
      <author>iseunghan</author>
      <guid isPermaLink="true">https://iseunghan.tistory.com/486</guid>
      <comments>https://iseunghan.tistory.com/486#entry486comment</comments>
      <pubDate>Tue, 5 Aug 2025 23:29:33 +0900</pubDate>
    </item>
    <item>
      <title>influxDB aggregateWindow 타임존 이슈 여정기</title>
      <link>https://iseunghan.tistory.com/485</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번 포스팅에 이어, 이번에는 influxdb의 UTC의 데이터를 &lt;code&gt;aggregateWindow&lt;/code&gt; 함수를 이용해 집계를 내다보니 KST 기준의 데이터와는 차이가 있어 추후에 실수를 방지하기 위해 삽질했던 경험들을 기록하고 해결한 경험을 소개하고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 환경&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 테스트를 위한 influxdb 구축은 따로 설명하지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;python 3.12&lt;/li&gt;
&lt;li&gt;influxdb v2.7.12 (docker)&lt;/li&gt;
&lt;li&gt;test dataset: &lt;a href=&quot;https://docs.influxdata.com/influxdb/v2/reference/sample-data/#noaa-water-sample-data&quot;&gt;noaa-water-sample-data&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;time range: &lt;code&gt;2019-08-17T00:00:00Z&lt;/code&gt; ~ &lt;code&gt;2019-09-17T22:00:00Z&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;measurement: [&lt;code&gt;average_temperature&lt;/code&gt;, &lt;code&gt;h2o_feet&lt;/code&gt;, &lt;code&gt;h2o_pH&lt;/code&gt;, &lt;code&gt;h2o_quality&lt;/code&gt;, &lt;code&gt;h2o_temperature&lt;/code&gt;]&lt;/li&gt;
&lt;li&gt;location(tag): [&lt;code&gt;coyote_creek&lt;/code&gt;, &lt;code&gt;santa_monica&lt;/code&gt;]&lt;/li&gt;
&lt;li&gt;field_key: [&lt;code&gt;degrees&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;level description&lt;/code&gt;, &lt;code&gt;pH&lt;/code&gt;, &lt;code&gt;water_level&lt;/code&gt;]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;aggregateWindow는 UTC 기준으로 집계가 이뤄진다&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 코드: main.py/aggregate_with_flux_query_timeSrc&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;influxdb에 모든 데이터는 UTC 기준으로 저장되어 있습니다. UTC와 KST는 9시간 차이가 납니다.&lt;br /&gt;그럼 다음 데이터는 어느 날짜 기준으로 합계되었는지 생각해봅시다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;from(bucket: &quot;iseunghan-test-bucket&quot;)
      |&amp;gt; range(start: 2019-08-17T00:00:00Z, stop: 2019-09-17T22:00:00Z)
      |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;_measurement&quot;] == &quot;average_temperature&quot;)
      |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;location&quot;] == &quot;santa_monica&quot;)
      |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;_field&quot;] == &quot;degrees&quot;)
      |&amp;gt; aggregateWindow(every: 1d, fn: mean, createEmpty: false)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상했던 결과가 다음과 같나요?&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                      _time     _value
0 2019-08-17 00:00:00+00:00  80.012500
1 2019-08-18 00:00:00+00:00  80.162500
2 2019-08-19 00:00:00+00:00  79.333333
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실행결과는 어떨까요? _time을 비교해보시면, 날짜가 하루 씩 밀린 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                      _time     _value
0 2019-08-18 00:00:00+00:00  80.012500
1 2019-08-19 00:00:00+00:00  80.162500
2 2019-08-20 00:00:00+00:00  79.333333
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;code&gt;aggregateWindow&lt;/code&gt;의 &lt;a href=&quot;https://docs.influxdata.com/flux/v0/stdlib/universe/aggregatewindow/#timesrc&quot;&gt;timeSrc&lt;/a&gt; 옵션의 기본값이 &lt;code&gt;_stop&lt;/code&gt;으로 설정되어 있어서 그렇습니다. 물론 개인마다 기준이 다르겠지만 현재 개발중인 서비스 관점에서는 실시간성으로도 집계가 반영되어야 하기 때문에 8월 17일 00시부터 8월 18일 00시 이전의 데이터의 집계는 8월 17일 날짜로 표기되어야 한다고 생각합니다. 즉, &lt;code&gt;timeSrc&lt;/code&gt;를 &lt;code&gt;_start&lt;/code&gt;로 설정하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 쿼리를 수정하면 원하는 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;from(bucket: &quot;iseunghan-test-bucket&quot;)
      |&amp;gt; range(start: 2019-08-17T00:00:00Z, stop: 2019-09-17T22:00:00Z)
      |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;_measurement&quot;] == &quot;average_temperature&quot;)
      |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;location&quot;] == &quot;santa_monica&quot;)
      |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;_field&quot;] == &quot;degrees&quot;)
      |&amp;gt; aggregateWindow(every: 1d, fn: mean, timeSrc: &quot;_start&quot;, createEmpty: false)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                      _time     _value
0 2019-08-17 00:00:00+00:00  80.012500
1 2019-08-18 00:00:00+00:00  80.162500
2 2019-08-19 00:00:00+00:00  79.333333
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 데이터를 통해서 위 결과가 잘 집계된건지 확인해보겠습니다.&lt;br /&gt;KST 기준으로 집계를 하기 위해 기간은 KST 기준 2019-08-17 자정부터 2019-09-17 22시까지 datetime 객체를 만들고, UTC 시간대로 변환해주었습니다. 이제 해당 시간대의 데이터를 가져온 뒤, pandas를 이용하여 _time을 kst_date로 변환한 뒤, 평균을 내었습니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;def aggregate_with_pandas():
    start_dt = datetime.datetime(2019, 8, 17, 0, 0, 0, tzinfo=datetime.timezone(timedelta(hours=9))).astimezone(datetime.UTC)
    stop_dt = datetime.datetime(2019, 9, 17, 22, 0, 0, tzinfo=datetime.timezone(timedelta(hours=9))).astimezone(datetime.UTC)

    df = client.query_api().query_data_frame(f'''
        from(bucket: &quot;iseunghan-test-bucket&quot;)
          |&amp;gt; range(start: {start_dt.isoformat()}, stop: {stop_dt.isoformat()})
          |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;_measurement&quot;] == &quot;average_temperature&quot;)
          |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;location&quot;] == &quot;santa_monica&quot;)
          |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;_field&quot;] == &quot;degrees&quot;)
        ''')

    df = df.drop(columns=[&quot;result&quot;, &quot;table&quot;, &quot;_measurement&quot;, &quot;location&quot;, &quot;_field&quot;, &quot;_start&quot;, &quot;_stop&quot;], errors=&quot;ignore&quot;)
    df[&quot;_time_kst&quot;] = pd.to_datetime(df[&quot;_time&quot;]).dt.tz_convert(&quot;Asia/Seoul&quot;)
    df[&quot;kst_date&quot;] = df[&quot;_time_kst&quot;].dt.date

    # 일자별 합계 집계
    daily_mean = df.groupby(&quot;kst_date&quot;)[&quot;_value&quot;].mean().reset_index()
    print(&quot;###### aggregate_with_pandas #######&quot;)
    display(daily_mean.head())
    display(&quot;.......&quot;)
    display(daily_mean.tail())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;###### aggregate_with_pandas #######
     kst_date     _value
0  2019-08-17  79.566667
1  2019-08-18  80.216667
2  2019-08-19  79.841667
3  2019-08-20  79.875000
4  2019-08-21  79.941667
.......
      kst_date     _value
27  2019-09-13  80.058333
28  2019-09-14  80.370833
29  2019-09-15  80.229167
30  2019-09-16  80.508333
31  2019-09-17  80.081818&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더와 테일부분에만 보아도 기존과 값이 다르단 것을 알 수 있습니다. 왜 이런 현상이 발생했을까요?&lt;br /&gt;바로 aggregateWindow는 UTC 기준으로 집계를 내기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/iseunghan/iseunghan-Lab/blob/main/python-influxdb-aggregate-align-timezone/images/utc-kst-range-diff.png?raw=true&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노란색 영역 UTC 기준으로 조회한 결과, 빨간색 영역은 노란색 영역을 KST 기준으로 &lt;code&gt;timeshift&lt;/code&gt;를 하여 각각 집계를 낸 것입니다.&lt;br /&gt;서로 다른 시간대 범위의 집계를 내고 있었던 것이었습니다!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;red range: &lt;code&gt;80.216667&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;yellow range: &lt;code&gt;80.162500&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼 range를 UTC timeShift 후 집계하면 되는건가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로는 아닙니다. 역시나 개발자는 왜 아닌지 직접 확인해야 직성에 풀리기 때문에 예제를 통해 살펴봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 시간을 9시간 KST로 지정하여 조회를 하였습니다. 이렇게 aware-timezone을 이용해 조회를 하면, influxdb가 알아서 UTC로 변환하여 조회하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;def aggregate_with_flux_range_timeshift():
    start_dt = datetime.datetime(2019, 8, 17, 0, 0, 0, tzinfo=datetime.timezone(timedelta(hours=9)))
    stop_dt = datetime.datetime(2019, 9, 17, 22, 0, 0, tzinfo=datetime.timezone(timedelta(hours=9)))

    df = client.query_api().query_data_frame(f'''
        from(bucket: &quot;iseunghan-test-bucket&quot;)
          |&amp;gt; range(start: {start_dt.isoformat()}, stop: {stop_dt.isoformat()})
          |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;_measurement&quot;] == &quot;average_temperature&quot;)
          |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;location&quot;] == &quot;santa_monica&quot;)
          |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;_field&quot;] == &quot;degrees&quot;)
          |&amp;gt; aggregateWindow(every: 1d, fn: mean, timeSrc: &quot;_start&quot;)
        ''')
    df = df.drop(columns=[&quot;result&quot;, &quot;table&quot;, &quot;_measurement&quot;, &quot;location&quot;, &quot;_field&quot;], errors=&quot;ignore&quot;)
    display(df.head())&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해를 돕고자 &lt;code&gt;aggregateWindow&lt;/code&gt;의 &lt;code&gt;createEmpty&lt;/code&gt;를 &lt;code&gt;true&lt;/code&gt;로 설정하여, 첫 row의 _time이 &lt;code&gt;2019-08-16 15:00:00+00:00&lt;/code&gt;라는 것을 강조하고 싶었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과를 보면, &lt;code&gt;_start&lt;/code&gt;와 &lt;code&gt;_stop&lt;/code&gt;은 KST에서 UTC로 &lt;code&gt;-9&lt;/code&gt;시간을 timeshift한 범위로 잘 조회했지만, &lt;code&gt;_time&lt;/code&gt;을 보면 결국은 00시 기준으로 조회가 되고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                     _start                     _stop                      _time     _value
0 2019-08-16 15:00:00+00:00 2019-09-17 13:00:00+00:00  2019-08-16 15:00:00+00:00        NaN
1 2019-08-16 15:00:00+00:00 2019-09-17 13:00:00+00:00  2019-08-17 00:00:00+00:00  80.012500
2 2019-08-16 15:00:00+00:00 2019-09-17 13:00:00+00:00  2019-08-18 00:00:00+00:00  80.162500
3 2019-08-16 15:00:00+00:00 2019-09-17 13:00:00+00:00  2019-08-19 00:00:00+00:00  79.333333
4 2019-08-16 15:00:00+00:00 2019-09-17 13:00:00+00:00  2019-08-20 00:00:00+00:00  80.137500
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 우리가 원하는 15시 기준으로 집계가 되게하려면 어떻게 해야할까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LocalTime에 맞게 aggregate를 할 수 있는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 LocalTime에 맞게 서버에서 올바른 응답을 줄 수 있도록 하는 방법 두가지를 소개하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) aggregateWindow(?offset: duration)을 활용한 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset 옵션에 대해서 공식문서에서 가져와봤습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset: Duration to shift the window boundaries by. Default is 0s. offset can be negative, indicating that the offset goes backwards in time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset은 윈도우의 경계를 지정된 시간만큼 이동시키는데 사용되며, 음수 값을 사용하면 윈도우를 과거로 이동시킬 수 있습니다.&lt;br /&gt;우리의 목표는 UTC 00시가 아닌 전날 15시 기준으로 집계를 내야하며 즉, -9시간을 파라미터로 넘겨주면 되는 것입니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/iseunghan/iseunghan-Lab/blob/main/python-influxdb-aggregate-align-timezone/images/utc-kst-range-diff.png?raw=true&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 위 그림과 동일하게 window를 KST 00시 기준으로 옮길 수 있는 것이죠!&lt;br /&gt;아래 실습 코드로 바로 확인해봅시다!&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;def aggregate_with_flux_offset():
    df = client.query_api().query_data_frame(f'''
        from(bucket: &quot;iseunghan-test-bucket&quot;)
          |&amp;gt; range(start: 2019-08-17T00:00:00+09:00, stop: 2019-09-17T22:00:00+09:00)
          |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;_measurement&quot;] == &quot;average_temperature&quot;)
          |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;location&quot;] == &quot;santa_monica&quot;)
          |&amp;gt; filter(fn: (r) =&amp;gt; r[&quot;_field&quot;] == &quot;degrees&quot;)
          |&amp;gt; aggregateWindow(every: 1d, fn: mean, timeSrc: &quot;_start&quot;, offset: -9h)
        ''')
    df = df.drop(columns=[&quot;result&quot;, &quot;table&quot;, &quot;_measurement&quot;, &quot;location&quot;, &quot;_field&quot;], errors=&quot;ignore&quot;)
    df[&quot;_time_kst&quot;] = pd.to_datetime(df[&quot;_time&quot;]).dt.tz_convert(&quot;Asia/Seoul&quot;)
    display(df.head())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 range의 &lt;code&gt;+09:00&lt;/code&gt; 타임존과 &lt;code&gt;offset: -9h&lt;/code&gt;로 인해 중복으로 -18시간 처리되어 조회되는게 아닌지 궁금해 하실 수 있는데 range는 조회되는 데이터의 범위를 지정해주는 것이고, offset은 집계를 낼 때 window의 위치를 옮기는 것뿐이라 이는 서로 다른 역할을 합니다. (KST 17일 00시는 16일 15시이므로 range를 전날 15시부터 잡는 것입니다!)&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;                     _start                       _stop                      _time                  _time_kst    _value
0 2019-08-16 15:00:00+00:00   2019-09-17 13:00:00+00:00  2019-08-16 15:00:00+00:00  2019-08-17 00:00:00+09:00 79.566667
1 2019-08-16 15:00:00+00:00   2019-09-17 13:00:00+00:00  2019-08-17 15:00:00+00:00  2019-08-18 00:00:00+09:00 80.216667
2 2019-08-16 15:00:00+00:00   2019-09-17 13:00:00+00:00  2019-08-18 15:00:00+00:00  2019-08-19 00:00:00+09:00 79.841667
3 2019-08-16 15:00:00+00:00   2019-09-17 13:00:00+00:00  2019-08-19 15:00:00+00:00  2019-08-20 00:00:00+09:00 79.875000
4 2019-08-16 15:00:00+00:00   2019-09-17 13:00:00+00:00  2019-08-20 15:00:00+00:00  2019-08-21 00:00:00+09:00 79.941667
... &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;_time&lt;/code&gt;과 &lt;code&gt;_time_kst&lt;/code&gt;를 살펴보시면, 드디어 원하던 KST 기준으로 집계가 정상적으로 이뤄졌습니다..!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) timezone.location을 활용한 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;offset&lt;/code&gt; 말고 또 다른 방법이 하나 더 있습니다. 바로 timezone 기능을 활용하는 것인데요. 먼저 timezone은 현재 타임존을 설정할 수 있는 편리한 flux 내장 함수라고 생각하시면 됩니다.&lt;br /&gt;사용 방법은 간단하게 timezone을 import한 뒤 fixed() 또는 location()를 사용하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import &quot;timezone&quot;

timezone.fixed(offset: -8h)// Returns {offset: -8h, zone: &quot;UTC&quot;}
timezone.location(name: &quot;America/Los_Angeles&quot;)// Returns {offset: 0ns, zone: &quot;America/Los_Angeles&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 이제 실습코드를 통해 offset 결과와 동일한지 살펴봅시다!&lt;br /&gt;기존 코드와 달라진 부분은 timezone 모듈을 임포트한 다음 현재 location을 Asia/Seoul로 지정해준 것입니다. 추가로 aggregatewindow의 offset은 제거해줍니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;def aggregate_with_location():
    df = client.query_api().query_data_frame(f'''
            import &quot;timezone&quot;
            option location = timezone.location(name: &quot;Asia/Seoul&quot;)
            ...
              |&amp;gt; aggregateWindow(every: 1d, fn: mean, timeSrc: &quot;_start&quot;)
            ''')
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                     _start                      _stop                      _time                 _time_kst    _value
0 2019-08-16 15:00:00+00:00  2019-09-17 13:00:00+00:00  2019-08-16 15:00:00+00:00 2019-08-17 00:00:00+09:00 79.566667 
1 2019-08-16 15:00:00+00:00  2019-09-17 13:00:00+00:00  2019-08-17 15:00:00+00:00 2019-08-18 00:00:00+09:00 80.216667 
2 2019-08-16 15:00:00+00:00  2019-09-17 13:00:00+00:00  2019-08-18 15:00:00+00:00 2019-08-19 00:00:00+09:00 79.841667 
3 2019-08-16 15:00:00+00:00  2019-09-17 13:00:00+00:00  2019-08-19 15:00:00+00:00 2019-08-20 00:00:00+09:00 79.875000 
4 2019-08-16 15:00:00+00:00  2019-09-17 13:00:00+00:00  2019-08-20 15:00:00+00:00 2019-08-21 00:00:00+09:00 79.941667 
...          &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과를 &lt;code&gt;1) aggregateWindow(?offset: duration)을 활용한 방법&lt;/code&gt;의 실행결과와 비교해보면 동일한 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며..&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 influxdb에 저장된 데이터는 모두 UTC로 저장된다는 사실이 어색하게 느껴져서 시간대를 맞추려 정말 많이 삽질했습니다. 이번에 influxdb에 저장된 시계열 데이터들을 가지고 여러 집계 API를 구현해야 했는데 실제 데이터(KST)와 쿼리로 조회한 집계 데이터가 불일치하는 것 같다는 의심에서부터 여기까지 오게되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 삽질 덕분에 앞으로 aggregateWindow를 활용하여 집계를 낼 때는 더는 실수하지 않고 정확한 집계를 낼 수 있을 것 같습니다. 추가로 서비스가 점점 확장되어 글로벌 서비스를 하게 되더라도 timezone 변수만 동적으로 바꿔준다면 클라이언트의 localTime에 맞는 정확한 집계 데이터를 제공할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;h1&gt;REFERENCES&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.influxdata.com/flux/v0/&quot;&gt;influxdb flux docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.influxdata.com/flux/v0/stdlib/universe/aggregatewindow/#offset&quot;&gt;influxdb aggregatewindow offset&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.influxdata.com/flux/v0/stdlib/timezone/location/&quot;&gt;influxdb timezone location&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  Python</category>
      <category>aggregatewindow</category>
      <category>aggregatewindow timezone</category>
      <category>influxdb</category>
      <category>influxdb 타임존 불일치</category>
      <category>OFFSET</category>
      <category>timezone.location</category>
      <author>iseunghan</author>
      <guid isPermaLink="true">https://iseunghan.tistory.com/485</guid>
      <comments>https://iseunghan.tistory.com/485#entry485comment</comments>
      <pubDate>Fri, 30 May 2025 00:04:55 +0900</pubDate>
    </item>
    <item>
      <title>[Spring MVC] Zip, UnZip 업로드 및 다운로드 API 구현 (feat. MultipartFile)</title>
      <link>https://iseunghan.tistory.com/484</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1950&quot; data-origin-height=&quot;1100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsdpvX/btsH8JhN7Wt/Wpge1irNbicmFjcm9lFNDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsdpvX/btsH8JhN7Wt/Wpge1irNbicmFjcm9lFNDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsdpvX/btsH8JhN7Wt/Wpge1irNbicmFjcm9lFNDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsdpvX%2FbtsH8JhN7Wt%2FWpge1irNbicmFjcm9lFNDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1950&quot; height=&quot;1100&quot; data-origin-width=&quot;1950&quot; data-origin-height=&quot;1100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발환경&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  전체 코드는 &lt;a href=&quot;https://github.com/iseunghan/iseunghan-Lab/tree/main/spring-zip-unzip-handling&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;를 참조해주세요.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;spring boot 3.3.1&lt;/li&gt;
&lt;li&gt;JDK 17&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Intro&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서는 MultipartFile 이라는 인터페이스를 통해 파일 업로드 기능을 제공하고 있습니다. ZIP, UnZIP 기능을 제공하는 유틸성 클래스를 함께 개발해보고, MultipartFile을 함께 사용하여 파일 업로드, 그리고 HttpServletResponse를 통해 파일을 클라이언트에게 스트림을 통해 내려줘서 다운로드할 수 있는 API를 개발 및 테스트 하는 방법에 대해서 배워보도록 하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;먼저 Multipartfile이 뭘까요?&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 파일을 전송할 때, HTTP body를 여러 부분(&lt;code&gt;Multipart Data&lt;/code&gt;)으로 나눠서 보내는데 이러한 Multipart Data 즉, Multipart 요청으로 받은 업로드된 파일을 말합니다. 해당 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/multipart/MultipartFile.html&quot;&gt;인터페이스&lt;/a&gt;를 통해 파일의 정보(사이즈, 파일 이름 등)를 쉽게 가져올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서는 요청으로 받은 파일 정보를 메모리 또는 디스크에 일시적으로 저장시켜 사용할 수 있게 해줍니다. 임시 데이터는 요청 처리가 끝날 때 정리됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Zip 구현&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 ZIP 기능을 먼저 구현해보겠습니다. API를 어떻게 설계하냐에 따라 달라지겠지만, 원하는 디렉토리 또는 파일을 제공받고 해당 디렉토리 또는 파일을 Zipping하는 간단한 기능을 구현했습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public static byte[] zipFile(File sourceFile) {
    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
        try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
            addFileToZip(sourceFile, zipOutputStream, null);
        }
        return byteArrayOutputStream.toByteArray();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 ZIP으로 쓰기 위해서는 파일 내부에 byte로 작성해야 합니다. ByteArrayOutputStream과 ZipOutputStream을 try-with-resource를 통해 열어줍니다. 그리고는 addFileToZip 메서드를 호출하는데요 핵심 로직은 해당 메서드 내부에 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private static void addFileToZip(File parentFile, ZipOutputStream zipOutputStream, String parentFolderName) {
    String currentPath = hasText(parentFolderName) ? parentFolderName + &quot;/&quot; + parentFile.getName() : parentFile.getName();
    if (parentFile.isDirectory()) {
        for (File f : Objects.requireNonNull(parentFile.listFiles())) {
            addFileToZip(f, zipOutputStream, currentPath);
        }
    } else {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        try (FileInputStream fileInputStream = new FileInputStream(parentFile)) {
            zipOutputStream.putNextEntry(new ZipEntry(currentPath));

            int length;
            while ((length = fileInputStream.read(buffer)) &amp;gt; 0) {
                zipOutputStream.write(buffer, 0, length);
            }
            zipOutputStream.closeEntry();
        } catch (FileNotFoundException e) {
            log.error(&quot;File not found: {}, Exception: {}&quot;, parentFile, e.getMessage());
            throw new RuntimeException(e);
        } catch (IOException e) {
            log.error(&quot;Error while zipping file: {}, Exception: {}&quot;, parentFile, e.getMessage());
            throw new RuntimeException(e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메서드는 재귀 함수처럼 사용됩니다. ZipOutputStream을 전달받아서 현재 경로를 먼저 생성합니다. (폴더가 여러 depth로 구성되어있는 것을 유지하기 위해)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 파일이 디렉토리라면? 재귀 호출을 하고, 파일이라면? 파일 스트림을 열어서 ZipEntry에 write 해줍니다. 여기서 주의할 점은 내부가 비어있다면 fileInputStream.read(buffer) 이 부분에서 예외가 발생합니다. 해당 부분은 try-catch를 통해 잡아주시던지 지금처럼 그냥 예외를 발생하던지 하시면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. Zip 기능 테스트&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
void directoryZippingTest() throws IOException {
    // given
    final Path zipFilePath = Path.of(&quot;src/test/resources/test-dir&quot;);
    final Path testZipPath = Path.of(&quot;src/test/resources/test.zip&quot;);
    Files.deleteIfExists(testZipPath);  // delete test-zip

    // when
    byte[] bytes = ZipUtils.zipFile(zipFilePath.toFile());
    Files.write(testZipPath, bytes);

    // then
    assertThat(testZipPath).exists();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
void fileZippingTest() throws IOException {
    // given
    final Path zipFilePath = Path.of(&quot;src/test/resources/한글이름_테스트.txt&quot;);
    final Path testZipPath = Path.of(&quot;src/test/resources/test2.zip&quot;);
    Files.deleteIfExists(testZipPath);  // delete test-zip

    // when
    byte[] bytes = ZipUtils.zipFile(zipFilePath.toFile());
    Files.write(testZipPath, bytes);

    // then
    assertThat(testZipPath).exists();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는 간단합니다. Zipping 할 파일 또는 디렉토리를 넘겨서 byte[]를 반환받은 다음, 해당 byte[]를 파일로 써주면 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. unZip 기능 구현&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unZip 기능은 다음과 같이 구현하였습니다. Zip 파일(zipFile) 또는 InputStream을 압축 해제할 디렉토리(dstPath)로 입력받아 처리합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public static void unZipFile(String dstPath, File zipFile) throws IOException {
    unZipFile(dstPath, new FileInputStream(zipFile), null);
}

public static void unZipFile(String dstPath, InputStream inputStream) throws IOException {
    unZipFile(dstPath, inputStream, null);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으론 InputStream으로 변환하여 핵심 로직을 수행하는 메서드로 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private static void unZipFile(String dstPath, InputStream inputStream, @Nullable Function&amp;lt;Path, Path&amp;gt; renameDuplicatedFilename) throws IOException {
  try (ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charset.forName(&quot;EUC-KR&quot;))) {
      ZipEntry entry;
      byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];

      while ((entry = zipInputStream.getNextEntry()) != null) {
          final String entryName = entry.getName();
          if (entry.isDirectory() || entryName.startsWith(&quot;__MACOSX&quot;)) continue;

          File entryFile;
          if (renameDuplicatedFilename == null) {
              entryFile = new File(dstPath, entryName);
          } else {
              Path path = Path.of(dstPath, entryName);
              entryFile = renameDuplicatedFilename.apply(path).toFile();
          }
          Files.createDirectories(entryFile.getParentFile().toPath());

          try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(entryFile))) {
              int bytesRead;
              while ((bytesRead = zipInputStream.read(buffer)) != -1) {
                  outputStream.write(buffer, 0, bytesRead);
              }
          }
          zipInputStream.closeEntry();
      }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달 받은 inputStream을 이용해서 ZipInputStream을 열어줍니다. 각 entry를 순회하면서 Directory거나 __MACOSX로 시작한다면 건너띄어줍니다. 이렇게 한 이유는 디렉토리에 속한 엔트리를 옮길 때 속한 디렉토리를 생성 시킬 것이기 때문입니다. 혹시라도 압축을 해제하는 곳에 동일한 이름의 파일이 존재한다면 예외가 발생할 수 있으니 renameDuplicatedFilename를 이용해서 rename을 할 수 있도록 설정할 수도 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. UnZip 기능 테스트&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
void unzipTest() throws IOException {
    // given
    Path dstPath = Path.of(&quot;src/test/resources/unzip-result&quot;);
    FileUtils.deleteDirectory(dstPath.toFile());
    final Path testZipPath = Path.of(&quot;src/test/resources/test.zip&quot;);

    // when
    ZipUtils.unZipFile(dstPath.toString(), testZipPath.toFile());

    // then
    assertThat(dstPath).exists();
    assertThat(Path.of(dstPath.toString(), &quot;test-dir&quot;, &quot;한글이름_테스트.txt&quot;)).exists();
    assertThat(Path.of(dstPath.toString(), &quot;test-dir&quot;, &quot;file2.txt&quot;)).exists();
    assertThat(Path.of(dstPath.toString(), &quot;test-dir&quot;, &quot;file3.txt&quot;)).exists();
    assertThat(Path.of(dstPath.toString(), &quot;test-dir&quot;, &quot;test-dir2&quot;, &quot;한글이름_테스트.txt&quot;)).exists();
    assertThat(Path.of(dstPath.toString(), &quot;test-dir&quot;, &quot;test-dir2&quot;, &quot;file2.txt&quot;)).exists();
    assertThat(Path.of(dstPath.toString(), &quot;test-dir&quot;, &quot;test-dir2&quot;, &quot;test-dir3&quot;, &quot;한글이름_테스트.txt&quot;)).exists();
    assertThat(Path.of(dstPath.toString(), &quot;test-dir&quot;, &quot;test-dir2&quot;, &quot;test-dir3&quot;, &quot;file2.txt&quot;)).exists();
    assertThat(Path.of(dstPath.toString(), &quot;test-dir&quot;, &quot;test-dir2&quot;, &quot;test-dir3&quot;, &quot;test-dir4&quot;, &quot;한글이름_테스트.txt&quot;)).exists();
    assertThat(Path.of(dstPath.toString(), &quot;test-dir&quot;, &quot;test-dir2&quot;, &quot;test-dir3&quot;, &quot;test-dir4&quot;, &quot;file2.txt&quot;)).exists();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unzip 테스트도 간단합니다. unzip하려는 Zip 파일과, 압축 해제할 디렉토리를 넘겨서 압축해제하고 내부에 파일들이 정상적으로 생성됐는지 검증 합니다. 테스트는 성공적으로 수행됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Directory 다운로드 API 구현&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@GetMapping(value = &quot;/api/v1/file-zipping-and-download&quot;)
public void downloadZipFile(
        @RequestParam(value = &quot;folderName&quot;) String folderName,
        HttpServletResponse response
) {
    try {
        byte[] bytes = ZipUtils.zipFile(Path.of(folderName).toFile());
        response.setContentType(&quot;application/zip&quot;);
        response.setHeader(&quot;Content-Disposition&quot;, &quot;attachment; filename=&quot;.concat(UUID.randomUUID().toString()).concat(&quot;.zip&quot;));
        response.getOutputStream().write(bytes);
    } catch (IOException e) {
        throw new RuntimeException(&quot;Failed to download zip file&quot;, e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드 하려는 디렉토리 경로를 입력받아 우리가 구현한 zipFile 메서드를 호출하여 반환 받은 byte[]를 HttpServletResponse의 outputStream을 통해 써주면 클라이언트는 자동적으로 다운받을 수 있습니다. 중요한 점은 ContentType과 Header를 올바르게 설정해줘야 합니다. 현재는 간단하게 테스트 할 목적으로 Controller에 로직을 작성했는데 따로 Service 레이어로 옮기는게 예외 처리 및 유지보수할 때도 편리할 것 입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. Directory 다운로드 API 테스트&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@WebMvcTest(controllers = ZipUtilController.class)
class ZipUtilControllerTest {
    @Autowired private MockMvc mockMvc;

        @DisplayName(&quot;Zip Download Test&quot;)
        @Test
        void downloadZipFileTest() throws Exception {
            // given

            // when &amp;amp; then
            mockMvc.perform(get(&quot;/api/v1/file-zipping-and-download&quot;)
                            .param(&quot;folderName&quot;, &quot;src/test/resources/test-dir&quot;))
                    .andDo(print())
                    .andExpect(status().isOk())
            ;
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MockMvc를 이용해서 zipFile Download 테스트를 할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Zip 파일 업로드 API 구현&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@PostMapping(value = &quot;/api/v1/zip-upload&quot;, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity&amp;lt;String&amp;gt; uploadZipFile(
        @RequestParam(&quot;dstPath&quot;) String dstPath,
        @RequestPart(&quot;file&quot;) MultipartFile file
) {
    try {
        ZipUtils.unZipFile(dstPath, file.getInputStream());
    } catch (IOException e) {
        return ResponseEntity.ok(&quot;failed to upload zip&quot;);
    }
    return ResponseEntity.ok(&quot;success to upload zip&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업로드 API는 Multipart를 이용해서 업로드를 할 수 있습니다. 현재 API 처럼 단일 MultipartFile로 구성해도 되고, List를 통해 여러 개를 받아서 내부에서 Zipping하여 저장할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 Zip 파일을 업로드 받는다고 가정하고 ZIp 파일을 받아서 우리가 만든 unZipFile 메서드를 통해 사용자로부터 입력받은 저장 경로에 압축해제 할 수 있도록 구현하였습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;

@DisplayName(&quot;Zip Upload Test&quot;)
@Test
void uploadZipFileTest() throws Exception {
    // given
    MockMultipartFile file = new MockMultipartFile(&quot;file&quot;, &quot;test.zip&quot;, MULTIPART_FORM_DATA_VALUE, (byte[]) null);

    // when &amp;amp; then
    mockMvc.perform(multipart(&quot;/api/v1/zip-upload&quot;)
                    .file(file)
                    .param(&quot;dstPath&quot;, &quot;test-dir&quot;))
            .andDo(print())
            .andExpect(status().isOk())
    ;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MultipartFile 업로드 API 테스트는 &lt;code&gt;MockMvcRequestBuilders.multipart&lt;/code&gt; 를 통해서 테스트할 수 있습니다. MultipartFile은 MockMultipartFile을 이용해서 Mocking하여 테스트하면 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 트러블 슈팅&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1. MacOS에서 생성한 zip은 내부에 디렉토리가 숨어있다? (&lt;code&gt;__**MAC_OS__**&lt;/code&gt;)&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MacOS에서는 Zip 파일들의 메타데이터들을 &lt;b&gt;&lt;code&gt;__MAC_OS__&lt;/code&gt;&lt;/b&gt; 내부에 저장합니다. (&lt;a href=&quot;https://stackoverflow.com/questions/53813711/whats-inside-the-macosx-hidden-folder-in-zip-files-created-in-mac-os&quot;&gt;stack overflow 참조&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 혹시 모를 Mac 유저들을 대비해 해당 폴더에 대해 예외처리해줘야 합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private static void unZipFile(String dstPath, InputStream inputStream, @Nullable Function&amp;lt;Path, Path&amp;gt; renameDuplicatedFilename) throws IOException {
  try (ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charset.forName(&quot;EUC-KR&quot;))) {
      ZipEntry entry;
            ...

      while ((entry = zipInputStream.getNextEntry()) != null) {
          final String entryName = entry.getName();
          if (entry.isDirectory() || entryName.startsWith(&quot;__MACOSX&quot;)) continue;

                    ...
      }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;entryName.startsWith(&quot;__MACOSX&quot;)&lt;/code&gt;를 통해 해당 이름으로 시작하는 폴더는 패스하게끔 예외 처리해줬습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/53813711/whats-inside-the-macosx-hidden-folder-in-zip-files-created-in-mac-os&quot;&gt;What's inside the __MACOSX hidden folder in zip files created in Mac OS&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jaimemin.tistory.com/2244&quot;&gt;[Java] zip 파일 내 구성 확인하는 코드&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2. 윈도우 브라우저에서는 확장자가 appication/zip이 아니라 application/x-zip-compressed이라고?&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;윈도우에서는 ZIP 파일을 업로드하면 브라우저에서 application/zip이 아닌 application/x-zip-compressed 값을 넘겨줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 업로드 형식을 제한한다면, 윈도우 사용자를 위해 application/x-zip-compressed Content-Type도 허용해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/4411757/zip-mime-types-when-to-pick-which-one&quot;&gt;zip mime types, when to pick which one&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3. 윈도우에서 업로드한 파일을 unZip했을 때, 내부 파일명들이 특수문자로 보일 때 (인코딩 형식 안맞음)&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니 윈도우에서는 EUC-KR 뭐시기로 인코딩을 했..다고 합니다..&lt;/p&gt;
&lt;p&gt;&lt;del&gt;하..&lt;/del&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;private static void unZipFile(String dstPath, InputStream inputStream, @Nullable Function&amp;lt;Path, Path&amp;gt; renameDuplicatedFilename) throws IOException {
  try (ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charset.forName(&quot;EUC-KR&quot;))) {
        ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인코딩만 적용해주면 되기 때문에 어려울 것은 없습니다. ZipInputStream을 생성할 때, &lt;code&gt;Charset.forName(&quot;EUC-KR&quot;)&lt;/code&gt; 인코딩을 통해 생성해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/4782662/java-char-set-encoding-problemfrom-utf8-to-cp866&quot;&gt;Java char set encoding problem(from UTF8 to cp866)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/20013091/java-unzip-error-malformed&quot;&gt;Java // unzip error :MALFORMED&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Outro&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서 제공해주는 MultipartFile 인터페이스를 이용하여 API 구현, 그리고 직접 구현해본 Zip, UnZip Util 클래스를 만들어보고 테스트를 해봤습니다. Zip, UnZip 유틸 클래스는 이미 유명한 라이브러리(ex. &lt;a href=&quot;http://apache.common.io&quot;&gt;apache.common.io&lt;/a&gt; 등)가 많은데요, 이런 라이브러리에 의존하는 것보단 기본 JDK API를 통해 구현하는게 나중에 라이브러리 보안 취약점, 업데이트의 부재 등등에 대처하기 어렵기 때문에 직접 구현하는게 좋다고 생각이 듭니다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;그렇게 대단한 기능은 아니지만요..&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 API 테스트 하는 부분이 조금 미흡하고 아쉽습니다. 이 부분은 더 좋은 방법이 있으면 추후에 업데이트 하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 글 읽어주셔서 감사합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;WidgetFloaterPanels&quot; class=&quot;LTRStyle&quot; style=&quot;display: none; text-align: left; direction: ltr; visibility: hidden;&quot; translate=&quot;no&quot;&gt;
&lt;div id=&quot;WidgetFloater&quot; style=&quot;display: none;&quot;&gt;
&lt;div id=&quot;WidgetLogoPanel&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;TRANSLATE with &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;img id=&quot;FloaterLogo&quot; /&gt; &lt;span&gt;x&lt;/span&gt;&lt;/div&gt;
&lt;div id=&quot;LanguageMenuPanel&quot;&gt;
&lt;div class=&quot;DDStyle_outer&quot;&gt;&lt;input id=&quot;LanguageMenu_svid&quot; style=&quot;display: none;&quot; autocomplete=&quot;on&quot; name=&quot;LanguageMenu_svid&quot; type=&quot;text&quot; value=&quot;en&quot; /&gt; &lt;input id=&quot;LanguageMenu_textid&quot; style=&quot;display: none;&quot; autocomplete=&quot;on&quot; name=&quot;LanguageMenu_textid&quot; type=&quot;text&quot; /&gt; &lt;span class=&quot;DDStyle&quot;&gt;English&lt;/span&gt;
&lt;div style=&quot;position: relative; text-align: left; left: 0;&quot;&gt;
&lt;div style=&quot;position: absolute; ;left: 0px;&quot;&gt;
&lt;div id=&quot;__LanguageMenu_popup&quot; class=&quot;DDStyle&quot; style=&quot;display: none;&quot;&gt;
&lt;table id=&quot;LanguageMenu&quot; border=&quot;0&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#ar&quot;&gt;Arabic&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#he&quot;&gt;Hebrew&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#pl&quot;&gt;Polish&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#bg&quot;&gt;Bulgarian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#hi&quot;&gt;Hindi&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#pt&quot;&gt;Portuguese&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#ca&quot;&gt;Catalan&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#mww&quot;&gt;Hmong Daw&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#ro&quot;&gt;Romanian&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#zh-CHS&quot;&gt;Chinese Simplified&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#hu&quot;&gt;Hungarian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#ru&quot;&gt;Russian&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#zh-CHT&quot;&gt;Chinese Traditional&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#id&quot;&gt;Indonesian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#sk&quot;&gt;Slovak&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#cs&quot;&gt;Czech&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#it&quot;&gt;Italian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#sl&quot;&gt;Slovenian&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#da&quot;&gt;Danish&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#ja&quot;&gt;Japanese&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#es&quot;&gt;Spanish&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#nl&quot;&gt;Dutch&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#tlh&quot;&gt;Klingon&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#sv&quot;&gt;Swedish&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#en&quot;&gt;English&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#ko&quot;&gt;Korean&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#th&quot;&gt;Thai&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#et&quot;&gt;Estonian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#lv&quot;&gt;Latvian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#tr&quot;&gt;Turkish&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#fi&quot;&gt;Finnish&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#lt&quot;&gt;Lithuanian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#uk&quot;&gt;Ukrainian&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#fr&quot;&gt;French&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#ms&quot;&gt;Malay&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#ur&quot;&gt;Urdu&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#de&quot;&gt;German&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#mt&quot;&gt;Maltese&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#vi&quot;&gt;Vietnamese&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#el&quot;&gt;Greek&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#no&quot;&gt;Norwegian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#cy&quot;&gt;Welsh&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#ht&quot;&gt;Haitian Creole&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a tabindex=&quot;-1&quot; href=&quot;#fa&quot;&gt;Persian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;img style=&quot;height: 7px; width: 17px; border-width: 0px; left: 20px;&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;script type=&quot;text/javascript&quot;&gt; var LanguageMenu; var LanguageMenu_keys=[&quot;ar&quot;,&quot;bg&quot;,&quot;ca&quot;,&quot;zh-CHS&quot;,&quot;zh-CHT&quot;,&quot;cs&quot;,&quot;da&quot;,&quot;nl&quot;,&quot;en&quot;,&quot;et&quot;,&quot;fi&quot;,&quot;fr&quot;,&quot;de&quot;,&quot;el&quot;,&quot;ht&quot;,&quot;he&quot;,&quot;hi&quot;,&quot;mww&quot;,&quot;hu&quot;,&quot;id&quot;,&quot;it&quot;,&quot;ja&quot;,&quot;tlh&quot;,&quot;ko&quot;,&quot;lv&quot;,&quot;lt&quot;,&quot;ms&quot;,&quot;mt&quot;,&quot;no&quot;,&quot;fa&quot;,&quot;pl&quot;,&quot;pt&quot;,&quot;ro&quot;,&quot;ru&quot;,&quot;sk&quot;,&quot;sl&quot;,&quot;es&quot;,&quot;sv&quot;,&quot;th&quot;,&quot;tr&quot;,&quot;uk&quot;,&quot;ur&quot;,&quot;vi&quot;,&quot;cy&quot;]; var LanguageMenu_values=[&quot;Arabic&quot;,&quot;Bulgarian&quot;,&quot;Catalan&quot;,&quot;Chinese Simplified&quot;,&quot;Chinese Traditional&quot;,&quot;Czech&quot;,&quot;Danish&quot;,&quot;Dutch&quot;,&quot;English&quot;,&quot;Estonian&quot;,&quot;Finnish&quot;,&quot;French&quot;,&quot;German&quot;,&quot;Greek&quot;,&quot;Haitian Creole&quot;,&quot;Hebrew&quot;,&quot;Hindi&quot;,&quot;Hmong Daw&quot;,&quot;Hungarian&quot;,&quot;Indonesian&quot;,&quot;Italian&quot;,&quot;Japanese&quot;,&quot;Klingon&quot;,&quot;Korean&quot;,&quot;Latvian&quot;,&quot;Lithuanian&quot;,&quot;Malay&quot;,&quot;Maltese&quot;,&quot;Norwegian&quot;,&quot;Persian&quot;,&quot;Polish&quot;,&quot;Portuguese&quot;,&quot;Romanian&quot;,&quot;Russian&quot;,&quot;Slovak&quot;,&quot;Slovenian&quot;,&quot;Spanish&quot;,&quot;Swedish&quot;,&quot;Thai&quot;,&quot;Turkish&quot;,&quot;Ukrainian&quot;,&quot;Urdu&quot;,&quot;Vietnamese&quot;,&quot;Welsh&quot;]; var LanguageMenu_callback=function(){ }; var LanguageMenu_popupid='__LanguageMenu_popup'; &lt;/script&gt;
&lt;/div&gt;
&lt;div id=&quot;CTFLinksPanel&quot;&gt;&lt;span&gt;&lt;a id=&quot;HelpLink&quot; title=&quot;Help&quot; href=&quot;https://go.microsoft.com/?linkid=9722454&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; &lt;img id=&quot;HelpImg&quot; /&gt;&lt;/a&gt; &lt;a id=&quot;EmbedLink&quot; title=&quot;Get this widget for your own site&quot;&gt;&lt;/a&gt; &lt;img id=&quot;EmbedImg&quot; /&gt; &lt;a id=&quot;ShareLink&quot; title=&quot;Share translated page with friends&quot;&gt;&lt;/a&gt; &lt;img id=&quot;ShareImg&quot; /&gt; &lt;/span&gt;&lt;/div&gt;
&lt;div id=&quot;FloaterProgressBar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;WidgetFloaterCollapsed&quot; style=&quot;display: none;&quot;&gt;&lt;span&gt;TRANSLATE with &lt;/span&gt;&lt;img id=&quot;CollapsedLogoImg&quot; /&gt;&lt;/div&gt;
&lt;div id=&quot;FloaterSharePanel&quot; style=&quot;display: none;&quot;&gt;
&lt;div id=&quot;ShareTextDiv&quot;&gt;&lt;span&gt; COPY THE URL BELOW &lt;/span&gt;&lt;/div&gt;
&lt;div id=&quot;ShareTextboxDiv&quot;&gt;&lt;input id=&quot;ShareTextbox&quot; name=&quot;ShareTextbox&quot; readonly=&quot;readonly&quot; type=&quot;text&quot; /&gt; &lt;!--a id=&quot;TwitterLink&quot; title=&quot;Share on Twitter&quot;&gt; &lt;img id=&quot;TwitterImg&quot; /&gt;&lt;/a&gt; &lt;a-- id=&quot;FacebookLink&quot; title=&quot;Share on Facebook&quot;&gt; &lt;img id=&quot;FacebookImg&quot; /&gt;&lt;/a--&gt; &lt;a id=&quot;EmailLink&quot; title=&quot;Email this translation&quot;&gt;&lt;/a&gt; &lt;img id=&quot;EmailImg&quot; /&gt;&lt;/div&gt;
&lt;div id=&quot;ShareFooter&quot;&gt;&lt;span&gt;&lt;a id=&quot;ShareHelpLink&quot;&gt;&lt;/a&gt; &lt;img id=&quot;ShareHelpImg&quot; /&gt;&lt;/span&gt; &lt;span&gt;&lt;a id=&quot;ShareBack&quot; title=&quot;Back To Translation&quot;&gt;&lt;/a&gt; Back&lt;/span&gt;&lt;/div&gt;
&lt;input id=&quot;EmailSubject&quot; name=&quot;EmailSubject&quot; type=&quot;hidden&quot; value=&quot;Check out this page in {0} translated from {1}&quot; /&gt; &lt;input id=&quot;EmailBody&quot; name=&quot;EmailBody&quot; type=&quot;hidden&quot; value=&quot;Translated: {0}%0d%0aOriginal: {1}%0d%0a%0d%0aAutomatic translation powered by Microsoft&amp;reg; Translator%0d%0ahttp://www.bing.com/translator?ref=MSTWidget&quot; /&gt; &lt;input id=&quot;ShareHelpText&quot; type=&quot;hidden&quot; value=&quot;This link allows visitors to launch this page and automatically translate it to {0}.&quot; /&gt;&lt;/div&gt;
&lt;div id=&quot;FloaterEmbed&quot; style=&quot;display: none;&quot;&gt;
&lt;div id=&quot;EmbedTextDiv&quot;&gt;&lt;span&gt;EMBED THE SNIPPET BELOW IN YOUR SITE&lt;/span&gt; &lt;a id=&quot;EmbedHelpLink&quot; title=&quot;Copy this code and place it into your HTML.&quot;&gt;&lt;/a&gt; &lt;img id=&quot;EmbedHelpImg&quot; /&gt;&lt;/div&gt;
&lt;div id=&quot;EmbedTextboxDiv&quot;&gt;&lt;input id=&quot;EmbedSnippetTextBox&quot; name=&quot;EmbedSnippetTextBox&quot; readonly=&quot;readonly&quot; type=&quot;text&quot; value=&quot;&amp;lt;div id='MicrosoftTranslatorWidget' class='Dark' style='color:white;background-color:#555555'&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;script type='text/javascript'&amp;gt;setTimeout(function(){var s=document.createElement('script');s.type='text/javascript';s.charset='UTF-8';s.src=((location &amp;amp;&amp;amp; location.href &amp;amp;&amp;amp; location.href.indexOf('https') == 0)?'https://ssl.microsofttranslator.com':'http://www.microsofttranslator.com')+'/ajax/v3/WidgetV3.ashx?siteData=ueOIGRSKkd965FeEGM5JtQ**&amp;amp;ctf=true&amp;amp;ui=true&amp;amp;settings=manual&amp;amp;from=en';var p=document.getElementsByTagName('head')[0]||document.documentElement;p.insertBefore(s,p.firstChild); },0);&amp;lt;/script&amp;gt;&quot; /&gt;&lt;/div&gt;
&lt;div id=&quot;EmbedNoticeDiv&quot;&gt;&lt;span&gt;Enable collaborative features and customize widget: &lt;a href=&quot;http://www.bing.com/widget/translator&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Bing Webmaster Portal&lt;/a&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div id=&quot;EmbedFooterDiv&quot;&gt;&lt;span&gt;&lt;a title=&quot;Back To Translation&quot;&gt;Back&lt;/a&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;script type=&quot;text/javascript&quot;&gt; var intervalId = setInterval(function () { if (MtPopUpList) { LanguageMenu = new MtPopUpList(); var langMenu = document.getElementById(LanguageMenu_popupid); var origLangDiv = document.createElement(&quot;div&quot;); origLangDiv.id = &quot;OriginalLanguageDiv&quot;; origLangDiv.innerHTML = &quot;&lt;span id='OriginalTextSpan'&gt;ORIGINAL: &lt;/span&gt;&lt;span id='OriginalLanguageSpan'&gt;&lt;/span&gt;&quot;; langMenu.appendChild(origLangDiv); LanguageMenu.Init('LanguageMenu', LanguageMenu_keys, LanguageMenu_values, LanguageMenu_callback, LanguageMenu_popupid); window[&quot;LanguageMenu&quot;] = LanguageMenu; clearInterval(intervalId); } }, 1); &lt;/script&gt;
&lt;/div&gt;</description>
      <category>  Spring</category>
      <category>MultipartFile</category>
      <category>spring unzip</category>
      <category>spring zip</category>
      <category>springboot multipartfile</category>
      <category>springboot unzip</category>
      <category>springboot zip</category>
      <category>unzip</category>
      <category>ZIP</category>
      <author>iseunghan</author>
      <guid isPermaLink="true">https://iseunghan.tistory.com/484</guid>
      <comments>https://iseunghan.tistory.com/484#entry484comment</comments>
      <pubDate>Sat, 22 Jun 2024 20:37:13 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] - ConfigurationProperties를 통해 프로퍼티 주입하기 (feat. @Value 상위 호환)</title>
      <link>https://iseunghan.tistory.com/483</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1950&quot; data-origin-height=&quot;1100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnZV4n/btsHGgtMNKj/k1fCFy2PEnd8A0OxDmsuU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnZV4n/btsHGgtMNKj/k1fCFy2PEnd8A0OxDmsuU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnZV4n/btsHGgtMNKj/k1fCFy2PEnd8A0OxDmsuU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnZV4n%2FbtsHGgtMNKj%2Fk1fCFy2PEnd8A0OxDmsuU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1950&quot; height=&quot;1100&quot; data-origin-width=&quot;1950&quot; data-origin-height=&quot;1100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml에 설정한 property값들을 @Value로 가져오기보다는 해당 설정을 다루는 객체를 하나 만들어두고 가져다 쓸 수 없을까? 하는 의문에서 출발하였습니다. 찾아보니 스프링 부트에서는 @ConfigurationProperties를 통해 Property를 객체에 저장할 수 있는 방법을 제공하고 있습니다. @Value 주입 방식보다 훨씬 더 안전하고 강력한 기능을 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Dependency 추가&lt;/h2&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. application.yml&lt;/h2&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;my:
    service:
        enabled: false
        remote-address: 192.168.1.1
        security:
            username: &quot;uname1&quot;
            password: &quot;pass1&quot;
            roles:
            - &quot;USER&quot;
            - &quot;ADMIN&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Property 주입 받을 클래스 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. Setter를 통한 주입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주입 받기 위한 클래스는 다음과 같이 선언할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Getter
@Setter
@ConfigurationProperties(prefix = &quot;my.service&quot;)
public class MyServiceProperty {
    private boolean enabled;
    private String remoteAddress;
    private Security security;

    @Getter
    @Setter
    public static class Security {
        private String username;
        private String password;
        private final List&amp;lt;String&amp;gt; roles = new ArrayList&amp;lt;&amp;gt;();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@ConfigurationProperties(&amp;rdquo;my.service&amp;rdquo;)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;my.service&lt;/code&gt;가 prefix로 설정되어 &lt;span style=&quot;text-align: start;&quot;&gt;설&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;정되어 그 하위 레벨에 선언된 key/value가 매칭되어 주입됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;public 접근지시자로 선언된 Setter가 꼭 필요합니다.&lt;/li&gt;
&lt;li&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빈 후처리기에서 Setter를 이용해 주입하기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. 생성자를 통한 주입&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
@ConfigurationProperties(prefix = &quot;my.service&quot;)
public class MyServiceProperty {
    private final boolean enabled;
    private final String remoteAddress;
    private final Security security;

    @Getter
    @RequiredArgsConstructor
    public static class Security {
        private final String username;
        private final String password;
        private final List&amp;lt;String&amp;gt; roles;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 멤버 변수들을 final로 선언하고 @RequiredArgsConstructor와 @ConstructorBinding을 통해 모든 필드를 매개변수로 하는 생성자를 생성하고 해당 생성자를 통해 프로퍼티를 주입받도록 설정할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3. 프로퍼티 바인딩에 대해서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서 사용했던 yml에서 remote-address는 어떻게 String remoteAddress 필드에 바인딩이 잘되었을까요? 저희가 따로 설정해준건 없는데 말이죠.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Desc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;my.service.remote-address&lt;/td&gt;
&lt;td&gt;권장되는 Kebab-case 형식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;my.service.remoteAddress&lt;/td&gt;
&lt;td&gt;Camel-case 형식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;my.service.remote_address&lt;/td&gt;
&lt;td&gt;Snake-case 형식&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml의 키의 포맷은 사람마다 다를 수 있기 때문에 스프링 부트는 이러한 프로퍼티의 키 여러 형식을 알아듣고 바인딩할 수 있도록 편리한 Relaxed Binding을 제공(위 3가지에 대해서 매핑을 시킵니다)합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Configuration 클래스에 등록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConfigurationProperties를 받을 클래스만 만들었다고 끝이 아닙니다. 해당 클래스에 주입을 시켜줄 수 있게 @Configuration이 붙은 클래스에 꼭 해당 클래스를 등록시켜줘야 정상작동합니다. 등록할 수 있는 방법은 총 2가지가 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1. @EnableConfigurationProperties&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@EnableConfigurationProperties(MyExampleProperty.class)
@Configuration
public class MyConfig {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration이 붙은 클래스에 @EnableConfigurationProperties 어노테이션에 해당 클래스를 등록해주면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2. @ConfigurationComponentScan&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@ConfigurationPropertiesScan
@Configuration
public class MyConfig {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EnableConfigurationProperties 어노테이션을 사용하면 설정파일이 늘어날 때마다 등록해줘야하는 불편함이 있습니다. 이러한 불편함을 해소하기 위한 방법으론 @ConfigurationPropertiesScan을 사용하면 됩니다! 기본적으로 해당 어노테이션이 붙은 클래스의 패키지를 스캔하기 때문에 다른 위치에 있다면 아래와 같이 추가해 주시면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@ConfigurationPropertiesScan({ &quot;com.example.app&quot;, &quot;com.example.another&quot; })
@Configuration
public class MyConfig {}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;번외 - @ConstructorBinding 테스트 시 주의할 점&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@SpringBootTest(classes = {AppV3Config.class, MyServiceProperty.class})
class MyServicePropertyTest {
    @Autowired
    private MyServiceProperty myServiceProperty;

    @Test
    void propertyTest() {
        assertThat(myServiceProperty.isEnabled()).isFalse();
        assertThat(myServiceProperty.getRemoteAddress()).isEqualTo(&quot;192.168.1.1&quot;);
        assertThat(myServiceProperty.getSecurity().getUsername()).isEqualTo(&quot;uname1&quot;);
        assertThat(myServiceProperty.getSecurity().getPassword()).isEqualTo(&quot;pass1&quot;);
        assertThat(myServiceProperty.getSecurity().getRoles()).containsExactlyInAnyOrder(&quot;USER&quot;, &quot;ADMIN&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 테스트는 왠지 통과해야 할 것 같은데.. 하지만 실행해보면?&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Failed to load ApplicationContext for [WebMergedContextConfiguration@3d49fd31 testClass = me.iseunghan.configurationproperty.config.v3_use_constructor.MyServicePropertyTest, locations = [], classes = [me.iseunghan.configurationproperty.config.v3_use_constructor.AppV3Config, me.iseunghan.configurationproperty.config.v3_use_constructor.MyServiceProperty], contextInitializerClasses = [], activeProfiles = [], propertySourceDescriptors = [], propertySourceProperties = [&quot;org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true&quot;], contextCustomizers = [org.springframework.boot.test.autoconfigure.actuate.observability.ObservabilityContextCustomizerFactory$DisableObservabilityContextCustomizer@1f, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizer@10cf09e8, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@6bca7e0d, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@19835e64, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@6c0d9d86, org.springframework.boot.test.context.SpringBootTestAnnotation@c8127d7e], resourceBasePath = &quot;src/main/webapp&quot;, contextLoader = org.springframework.boot.test.context.SpringBootContextLoader, parent = null]
java.lang.IllegalStateException: Failed to load ApplicationContext for [WebMergedContextConfiguration@3d49fd31 testClass = me.iseunghan.configurationproperty.config.v3_use_constructor.MyServicePropertyTest, locations = [], classes = [me.iseunghan.configurationproperty.config.v3_use_constructor.AppV3Config, me.iseunghan.configurationproperty.config.v3_use_constructor.MyServiceProperty], contextInitializerClasses = [], activeProfiles = [], propertySourceDescriptors = [], propertySourceProperties = [&quot;org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true&quot;], contextCustomizers = [org.springframework.boot.test.autoconfigure.actuate.observability.ObservabilityContextCustomizerFactory$DisableObservabilityContextCustomizer@1f, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizer@10cf09e8, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@6bca7e0d, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@19835e64, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@6c0d9d86, org.springframework.boot.test.context.SpringBootTestAnnotation@c8127d7e], resourceBasePath = &quot;src/main/webapp&quot;, contextLoader = org.springframework.boot.test.context.SpringBootContextLoader, parent = null]
...
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myServiceProperty': Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'boolean' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;No qualifying bean of type 'boolean' available&lt;/code&gt; .. 예?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;private final boolean enabled&lt;/code&gt; 필드를 빈이라고 착각하고 찾다가 실패한 것으로 추정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서를 찾아보니 다음과 같이 친절하게 적어놨습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;To use constructor binding the class must be enabled using &lt;code&gt;@EnableConfigurationProperties&lt;/code&gt; or configuration property scanning. You cannot use constructor binding with beans that are created by the regular Spring mechanisms (for example &lt;code&gt;@Component&lt;/code&gt; beans, beans created by using &lt;code&gt;@Bean&lt;/code&gt;methods or beans loaded by using &lt;code&gt;@Import&lt;/code&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Import와 같이 빈을 로드하면, ConstructorBinding을 사용할 수 없다고 친절하게 알려줬네요.. ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringbBootTest(classes={})가 @Import 처럼 동작하여서 그런 것 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TestConfig를 만들어서 테스트!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 에러를 해결하기 위해 테스트용 Config를 생성하여 ComponentScan을 v3에 대해서만 지정해주고 테스트에서는 해당 Config만 로드해서 돌려보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@ComponentScan(basePackages = {&quot;me.iseunghan.configurationproperty.config.v3_use_constructor&quot;})
@Configuration
public class TestV3Config {}

@SpringBootTest(classes = {TestV3Config.class})
class MyServicePropertyTest {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;601&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BQOXF/btsHBUxvzlN/dDScn44fF0P6krhxWFUwkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BQOXF/btsHBUxvzlN/dDScn44fF0P6krhxWFUwkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BQOXF/btsHBUxvzlN/dDScn44fF0P6krhxWFUwkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBQOXF%2FbtsHBUxvzlN%2FdDScn44fF0P6krhxWFUwkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;601&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;601&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 테스트가 성공하는 것을 확인할 수 있습니다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;figure id=&quot;og_1716972857282&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;https://docs.spring.io/spring-boot/redirect.html?page=features&quot; data-og-description=&quot;&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties&quot; data-og-url=&quot;https://docs.spring.io/spring-boot/redirect.html?page=features&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;https://docs.spring.io/spring-boot/redirect.html?page=features&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>  Spring</category>
      <category>application.properties 주입</category>
      <category>application.yml 주입</category>
      <category>configurationcomponentscan</category>
      <category>configurationproperties</category>
      <category>ConstructorBinding</category>
      <category>enableconfigurationproperties</category>
      <category>spring property injection</category>
      <category>Value</category>
      <author>iseunghan</author>
      <guid isPermaLink="true">https://iseunghan.tistory.com/483</guid>
      <comments>https://iseunghan.tistory.com/483#entry483comment</comments>
      <pubDate>Sat, 25 May 2024 22:50:02 +0900</pubDate>
    </item>
    <item>
      <title>시계열 데이터를 처리하는 InfluxDB에 대해서 알아보자</title>
      <link>https://iseunghan.tistory.com/482</link>
      <description>&lt;aside&gt;⚙ 아래는 InfluxDB v2.x 기준으로 설명합니다. v2.x부터 용어 쿼리들이 크게 변경되었기 때문에 v1.x 버전과는 용어적으로 차이가 있을 수 있습니다.&lt;/aside&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;influxDB를 알아보기 전 시계열 데이터란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnD2ta/btsHtOjEHNo/l3jLZpaOqbBGhCa3ROy7H1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnD2ta/btsHtOjEHNo/l3jLZpaOqbBGhCa3ROy7H1/img.png&quot; data-alt=&quot;https://www.influxdata.com/what-is-time-series-data/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnD2ta/btsHtOjEHNo/l3jLZpaOqbBGhCa3ROy7H1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnD2ta%2FbtsHtOjEHNo%2Fl3jLZpaOqbBGhCa3ROy7H1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1402&quot; height=&quot;936&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.influxdata.com/what-is-time-series-data/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시계열은 일정 기간 동안 수집되고 시간순으로 정렬된 데이터 요소의 모음입니다. 시계열의 주요 특징은 인덱싱되거나 시간 순서대로 나열된다는 것인데 그래서 그래프에 시계열 데이터를 시각화 할 때 중요한 축은 시간이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시계열 데이터는 다음과 같은 곳에서 사용될 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기상 예보&lt;/li&gt;
&lt;li&gt;주식&lt;/li&gt;
&lt;li&gt;센서 데이터&lt;/li&gt;
&lt;li&gt;일(월,연)간 구독자&lt;/li&gt;
&lt;li&gt;서버 자원 모니터링 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 들어 스마트 팩토리, 빅데이터, 코인, 주식 등이 핫한데 이런 데이터들의 특징은 시간순으로 인덱싱이 중요하고, 대용량 데이터라는 점입니다. 이러한 데이터들을 핸들링 하는 시계열 데이터베이스 중 InfluxDB가 도대체 어떠한 식으로 데이터를 처리하고 저장하여 빠른 속도를 낼 수 있는지 왜 이렇게 유명하고 많이들 사용하는지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;influxDB란? (Data Principle)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;influxdb는 시계열 데이터를 다룰 수 있는 가장 유명한 시계열 데이터베이스(time-series database)입니다. InfluxDB의 특징을 나열하면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간순으로 구성된 값들의 집합(Point)들을 저장&lt;/li&gt;
&lt;li&gt;수 초, 나노초의 엄청난 양의 데이터를 저장해야하기 때문에, 쓰기에 아주 특화됨&lt;/li&gt;
&lt;li&gt;쿼리 및 쓰기 성능을 높이기 위해 업데이트 및 삭제 기능은 엄격하게 &lt;b&gt;제한&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;SQL과 유사한 InfluxQL을 제공&lt;/li&gt;
&lt;li&gt;Continuous Query 기능 제공&lt;/li&gt;
&lt;li&gt;Tag라는 것을 이용해 인덱싱 제공&lt;/li&gt;
&lt;li&gt;REST API 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Storage Engine&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서는 Storage Engine을 다음과 같이 설명합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InfluxDB 스토리지 엔진은 다음을 보장합니다.&lt;br /&gt;1. 데이터가&lt;span&gt; &lt;/span&gt;디스크에&lt;span&gt; &lt;/span&gt;안전하게&lt;span&gt; &lt;/span&gt;기록됩니다&lt;span&gt;. &lt;br /&gt;&lt;/span&gt;2. 쿼리된&lt;span&gt; &lt;/span&gt;데이터는&lt;span&gt; &lt;/span&gt;완전하고&lt;span&gt; &lt;/span&gt;올바르게&lt;span&gt; &lt;/span&gt;반환됩니다&lt;span&gt;.&lt;br /&gt;&lt;/span&gt;3. 데이터가&lt;span&gt; &lt;/span&gt;정확하고&lt;span&gt; &lt;/span&gt;성능이&lt;span&gt; &lt;/span&gt;우수합니다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 위와 같이 설명할 수 있는지 내부구조를 살펴보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Writing Data (API &amp;rarr; Disk)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토리지 엔진은 HTTP API 요청을 수신한 시점부터 데이터를 처리합니다. Point들을 &lt;b&gt;WAL에 기록&lt;/b&gt;함과 동시에 &lt;b&gt;메모리 내 캐시에 기록&lt;/b&gt;시켜 즉시 쿼리할 수 있게 대비합니다. 메모리 내 캐시는 &lt;b&gt;TSM 파일 형식&lt;/b&gt;으로 주기적으로 &lt;b&gt;디스크에 기록&lt;/b&gt;됩니다. TSM 파일이 누적되면 스토리지 엔진은 누적된 파일을 &lt;b&gt;더 높은 수준의 TSM 파일로 결합&lt;/b&gt;하고 &lt;b&gt;압축&lt;/b&gt;합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Write Ahead Log (WAL)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL은 스토리지 엔진이 다시 시작될 때 influxDB 데이터를 유지할 수 있게 내구성을 보장해줍니다. 예기치 않은 스토리지 엔진의 종료가 있을 경우 재시작 될 때 WAL과 Storage Engine의 Sync를 맞춰 데이터의 내구성을 보장합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;쓰기 요청은 WAL 파일의 끝에 추가&lt;/li&gt;
&lt;li&gt;데이터는 &lt;code&gt;fsync()&lt;/code&gt;를 사용하여 디스크에 기록&lt;/li&gt;
&lt;li&gt;in-memory 캐시에 업데이트&lt;/li&gt;
&lt;li&gt;데이터가 디스크에 성공적으로 기록되면 쓰기 요청이 성공했음을 확인하는 응답 표시&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;fsync()&lt;/code&gt;는 파일을 가져와서 보류 중인 쓰기를 디스크까지 푸시합니다. 시스템 호출로서, &lt;code&gt;fsync()&lt;/code&gt;에는 계산 비용이 많이 들지만 데이터가 디스크에서 안전하다는 것을 보장하는 커널 컨텍스트 스위치가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토리지 엔진이 다시 시작되면 WAL 파일을 인메모리 데이터베이스로 다시 읽어들입니다. 그런 다음 influxDB는 &lt;code&gt;/read&lt;/code&gt; 엔드포인트에 대한 요청 응답.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시 (Cache)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시는 WAL에 저장된 데이터를 in-memory에 복제한 것이므로, WAL과 캐시는 별개의 엔티티며 서로 상호작용하지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키(measurement, tag set, unique fields)별로 구성된 포인트는 시간 순으로 저장됩니다.(압축 X)&lt;/li&gt;
&lt;li&gt;스토리지 엔진이 재 시작될 때마다 WAL에서 가져와서 캐싱해둡니다. 쿼리를 하면 캐시에서 데이터를 조회하고 TSM 파일에 저장된 데이터와 병합됩니다. (데이터의 캐시를 오래 유지하고 싶다면 maxSize를 늘리면 됨)&lt;/li&gt;
&lt;li&gt;TSM 파일에 기록되고 있는 캐시 개체를 캐시 스냅샷이라고 합니다.&lt;/li&gt;
&lt;li&gt;스토리지 엔진에 대한 쿼리는 캐시의 데이터를 TSM 파일의 데이터와 &lt;code&gt;merge&lt;/code&gt;(아래 TSM의 저장되는 형태를 보시면 이해가 가실겁니다)해서 응답합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Time-Structured Merge Tree (TSM)&lt;/h3&gt;
&lt;aside&gt;  좀 더 자세한 내용은 [공식문서](https://docs.influxdata.com/resources/videos/tsm-engine/)를 참고해주세요!&lt;/aside&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2242&quot; data-origin-height=&quot;1271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PlWof/btsHtoMwKed/L4ZeziZJ7m3TFalhqn0MX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PlWof/btsHtoMwKed/L4ZeziZJ7m3TFalhqn0MX0/img.png&quot; data-alt=&quot;https://docs.influxdata.com/resources/videos/tsm-engine/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PlWof/btsHtoMwKed/L4ZeziZJ7m3TFalhqn0MX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPlWof%2FbtsHtoMwKed%2FL4ZeziZJ7m3TFalhqn0MX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2242&quot; height=&quot;1271&quot; data-origin-width=&quot;2242&quot; data-origin-height=&quot;1271&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.influxdata.com/resources/videos/tsm-engine/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토리지 엔진은 TSM 데이터 형식을 사용합니다. 대용량의 시계열 데이터를 효율적으로 압축, 저장하기 위해 field value를 &lt;b&gt;series-key&lt;/b&gt;별로 그룹화한 다음 해당 field value를 시간별로 정렬합니다. (&lt;b&gt;series-key&lt;/b&gt;는 measurement, tag key, tag value, field key를 의미합니다)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TSM 형태로 저장하면 시리즈 키로 읽고 관련없는 데이터를 생략할 수 있으므로 쿼리 성능이 뛰어납니다.&lt;/li&gt;
&lt;li&gt;TSM 파일에 최종적으로 안전하게 저장되면 &lt;code&gt;WAL&lt;/code&gt;과 &lt;code&gt;캐시&lt;/code&gt;가 지워집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TSM 파일은 압축된 series data를 열 형식으로 저장합니다. 예를 들어 다음과 같은 데이터가 있다고 가정해봅시다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;measurement: weather

| timestamp | fields: wind_speed | fields: wind_direction |
| --------- | ------------------ | ---------------------- |
|   10:15   |         3.5        |           378          |
|   10:20   |         3.7        |           380          |&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 데이터는 실제로는 다음과 같이 TSM 형태로 디스크에 저장됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[measurement, tags, field key]
-----------------------------------
[weather, wind_speed]
  10:15 , 3.5
  10:20 , 3.7

[weather, wind_direction]
  10:15 , 378
  10:20 , 380&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;series-key(measurement, tag key, tag value, field key)별로 시간 순서대로 저장시킵니다. 그러니까 하나의 데이터가 총 2개의 TSM 데이터 셋으로 분리되어 저장된다고 생각하시면 편합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Continuous Query &amp;amp; Retension Policy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;influxdb의 핵심적인 기능 두가지입니다. 대용량의 시계열 데이터이다 보니까 저장공간 관리 측면과 데이터들을 효율적으로 관리할 수 있게 기능들을 제공해주는 것 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Continous Query&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bThcpm/btsHuK1GOMy/MJHdx75qkxuTHitladwQW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bThcpm/btsHuK1GOMy/MJHdx75qkxuTHitladwQW1/img.png&quot; data-alt=&quot;https://www.influxdata.com/blog/continuous-queries-in-influxdb-part-i/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bThcpm/btsHuK1GOMy/MJHdx75qkxuTHitladwQW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbThcpm%2FbtsHuK1GOMy%2FMJHdx75qkxuTHitladwQW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;433&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;433&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.influxdata.com/blog/continuous-queries-in-influxdb-part-i/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;influxdb는 데이터를 처리하여 새롭게 저장하는 Down Sampling(다운 샘플링)을 일정 주기마다 실행되도록 하는 기능인 Continuous Query를 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 기능을 이용해서 일정 주기마다 데이터들을 다운 샘플링 하여 미리 데이터를 구성해두면 &amp;rarr; 전체 데이터를 조회하는 것보다 훨씬 더 효율적이고 빠르게 쿼리하여 응답할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Retension Policy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량의 데이터가 쌓이고 나중에 저장공간이 부족하여 데이터를 저장하지 못하면 큰일입니다. 시계열 데이터가 대량으로 쌓이기 때문에 influxdb에서는 이 보존 정책을 제공하고 있습니다. (기본값: 영구저장)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 값은 영구적으로 계속 저장하는 방식으로 설정됩니다. 그렇기 때문에 보존 정책을 설정하여 오래된 데이터를 관리해주는 것이 좋습니다. &amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;del&gt;안그러면 데이터가 엄청나게 쌓여서 저장공간 부족하면 influxdb 죽습니다..&lt;/del&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테이블 구조&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RDB&lt;/th&gt;
&lt;th&gt;InfluxDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;database&lt;/td&gt;
&lt;td&gt;influxdb instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table&lt;/td&gt;
&lt;td&gt;measurement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;column&lt;/td&gt;
&lt;td&gt;key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;indexed column&lt;/td&gt;
&lt;td&gt;tag key (only String)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unindexed column&lt;/td&gt;
&lt;td&gt;field key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;row&lt;/td&gt;
&lt;td&gt;point&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Database&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In InfluxDB 2.2, a database represents the InfluxDB instance as a whole.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;influxDB 2.2에서는 전체 influxDB 인스턴스를 하나의 데이터베이스로 본다고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Measurement&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u72kf/btsHtjYPz6S/kJyZkZBbk05VpOSQrrLia1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u72kf/btsHtjYPz6S/kJyZkZBbk05VpOSQrrLia1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u72kf/btsHtjYPz6S/kJyZkZBbk05VpOSQrrLia1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu72kf%2FbtsHtjYPz6S%2FkJyZkZBbk05VpOSQrrLia1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1518&quot; height=&quot;510&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;measurement는 RDB에서 Table과 같습니다.&lt;/li&gt;
&lt;li&gt;어떤 값이 측정되는지에 대해 나타내는 지표가 됩니다. (예를들어, 위 테이블처럼 인구조사에 대해서 데이터를 수집한다면, measurement는 인구조사(census)가 되겠죠)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TimeStamp&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5xtyd/btsHsYm1EYZ/OInvPu9zkWuMQSZXoQwrk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5xtyd/btsHsYm1EYZ/OInvPu9zkWuMQSZXoQwrk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5xtyd/btsHsYm1EYZ/OInvPu9zkWuMQSZXoQwrk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5xtyd%2FbtsHsYm1EYZ%2FOInvPu9zkWuMQSZXoQwrk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1520&quot; height=&quot;508&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시계열 데이터베이스에서 가장 중요한 키값인 &lt;code&gt;_time&lt;/code&gt; 컬럼에 저장되는 값입니다. 당연히 인덱싱이 됩니다.&lt;/li&gt;
&lt;li&gt;Timestamp는 &lt;b&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc3339&quot;&gt;RFC 3339&lt;/a&gt;&lt;/b&gt;(for example: &lt;code&gt;2020-01-01T00:00:00.00Z&lt;/code&gt;)을 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Field Key, Field Value&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsvfum/btsHuxBqHQo/9kB4T9hH4D5zF6oRQhd5hK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsvfum/btsHuxBqHQo/9kB4T9hH4D5zF6oRQhd5hK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsvfum/btsHuxBqHQo/9kB4T9hH4D5zF6oRQhd5hK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbsvfum%2FbtsHuxBqHQo%2F9kB4T9hH4D5zF6oRQhd5hK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1502&quot; height=&quot;498&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Required) 필수값입니다. 실제 저장하려는 키/값을 field에 넣으면 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Field는 Tag와 다르게 인덱싱이 안됩니다. 그렇기 때문에 Field를 이용해 필터링을 걸면 성능 저하가 심하기 때문에 인덱싱이 필요하다면 Tag를 사용하시길 바랍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Tag Key, Tag Value&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqS3TY/btsHu2A2zar/UWIBM67KJnQvzrWUK3Tzk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqS3TY/btsHu2A2zar/UWIBM67KJnQvzrWUK3Tzk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqS3TY/btsHu2A2zar/UWIBM67KJnQvzrWUK3Tzk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqS3TY%2FbtsHu2A2zar%2FUWIBM67KJnQvzrWUK3Tzk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1512&quot; height=&quot;508&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Optional) 필수값은 아니지만 인덱싱이 되기 때문에 Tag를 넣어서 쿼리 성능을 높이는 것이 베스트입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tag Key는 여러개를 설정할 수 있습니다.&lt;/li&gt;
&lt;li&gt;일반적으로 쿼리를 위한 메타데이터들을 저장합니다. (예: 지역, 타입 등등)&lt;/li&gt;
&lt;li&gt;Tag Key는 무조건 String형입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.influxdata.com/influxdb/v2/reference/internals/storage-engine&quot;&gt;https://docs.influxdata.com/influxdb/v2/reference/internals/storage-engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.influxdata.com/influxdb/v2.2/reference/key-concepts/data-elements/&quot;&gt;https://docs.influxdata.com/influxdb/v2.2/reference/key-concepts/data-elements/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.influxdata.com/influxdb/v2/reference/key-concepts/design-principles/&quot;&gt;https://docs.influxdata.com/influxdb/v2/reference/key-concepts/design-principles/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.influxdata.com/influxdb/v2.2/reference/syntax/line-protocol/#elements-of-line-protocol&quot;&gt;https://docs.influxdata.com/influxdb/v2.2/reference/syntax/line-protocol/#elements-of-line-protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://musma.github.io/2019/07/08/getting-started-with-influxdb-time-series-database.html&quot;&gt;https://musma.github.io/2019/07/08/getting-started-with-influxdb-time-series-database.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mangkyu.tistory.com/190&quot;&gt;https://mangkyu.tistory.com/190&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=a00l1GxJszM&quot;&gt;https://www.youtube.com/watch?v=a00l1GxJszM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://musma.github.io/2019/07/08/getting-started-with-influxdb-time-series-database.html&quot;&gt;https://musma.github.io/2019/07/08/getting-started-with-influxdb-time-series-database.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://narup.tistory.com/169&quot;&gt;https://narup.tistory.com/169&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://andro-jinu.tistory.com/entry/InfluxDB2?category=924198&quot;&gt;https://andro-jinu.tistory.com/entry/InfluxDB2?category=924198&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@jee-9/InfluxDB%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-1&quot;&gt;https://velog.io/@jee-9/InfluxDB가-무엇인가-1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  Databases</category>
      <category>continuous query</category>
      <category>influxdb</category>
      <category>retension policy</category>
      <category>time-structured merge tree</category>
      <category>TSM</category>
      <category>Wal</category>
      <category>Write Ahead Log</category>
      <category>시계열 데이터</category>
      <author>iseunghan</author>
      <guid isPermaLink="true">https://iseunghan.tistory.com/482</guid>
      <comments>https://iseunghan.tistory.com/482#entry482comment</comments>
      <pubDate>Sat, 18 May 2024 23:52:48 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Cluster(Broker), Zookeeper에 대해서 이해하기</title>
      <link>https://iseunghan.tistory.com/481</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka란 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 요즘 MSA에 필수적으로 사용되는 기술입니다. 이 카프카란 뭐고 왜 필요할까요? 카프카는 링크드인이라는 회사에서 하루에 1조 몇천억이 넘는 대량의 메시지들을 처리하기 위해 개발되었습니다. 링크드인이 사용중이라면 대용량 메시지 처리를 할 수 있고 안정성 또한 높을 것이라고 기대가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 Messaging Queue라고도 합니다. 간단하게 말해서 메시지를 보내는 쪽이 있고 그 메시지를 관리하는 주체가 있고, 또 그 메시지를 수신하는 쪽이 있을 겁니다. Messaging Queue는 Pub/Sub Model을 의미하는데 카프카 말고도 여러 MQ들이 있는데 카프카의 차별화된 점은 무엇인지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Messaging Queue: 카프카를 알아보기 전에&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btdC4o/btsGXy9Le65/T8XLFKZFPqMlFFQaZemCT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btdC4o/btsGXy9Le65/T8XLFKZFPqMlFFQaZemCT1/img.png&quot; data-alt=&quot;https://ably.com/topic/pub-sub&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btdC4o/btsGXy9Le65/T8XLFKZFPqMlFFQaZemCT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtdC4o%2FbtsGXy9Le65%2FT8XLFKZFPqMlFFQaZemCT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;778&quot; height=&quot;459&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://ably.com/topic/pub-sub&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publish-Subscribe Model은 메시지를 만들어내는 Producer(생산자), 소비하는 Consumer(소비자) 이 둘을 중재하는 Broker(브로커)로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Topic이라는 개념이 등장하는데, 우리가 유튜브의 어떤 채널을 구독하면 해당 채널 관리자(Producer)가 영상을 업로드하면 구독자들에게 알림이 오듯이 유튜브 채널은 Topic이 되고, 해당 채널을 구독하는 우리는 Consumer가 되는 것입니다. 특정 사용자에게 전송한다고 생각을 할 수 있지만 Pub/Sub Model에서는 구독자들의 대한 정보는 모르고 Topic에 대한 정보만 알고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Topic, Partition 그리고 Segment File&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 1.png&quot; data-origin-width=&quot;3401&quot; data-origin-height=&quot;1874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lQsGD/btsGXZsuN9x/SCtjzuTNKgHa6K5CyMWot0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lQsGD/btsGXZsuN9x/SCtjzuTNKgHa6K5CyMWot0/img.png&quot; data-alt=&quot;https://www.scaler.com/topics/kafka-tutorial/kafka-partitions/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lQsGD/btsGXZsuN9x/SCtjzuTNKgHa6K5CyMWot0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlQsGD%2FbtsGXZsuN9x%2FSCtjzuTNKgHa6K5CyMWot0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3401&quot; height=&quot;1874&quot; data-filename=&quot;Untitled 1.png&quot; data-origin-width=&quot;3401&quot; data-origin-height=&quot;1874&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.scaler.com/topics/kafka-tutorial/kafka-partitions/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지는 Topic이라는 개념을 통해 카테고리화 됩니다. 특정 Topic으로 들어온 메시지들은 Queue 같은 자료구조에 하나씩 순차적으로 쌓이게 되는데 이때 내부에는 좀 더 세부화된 Partition과 Offset이라는 개념이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Topic은 여러 개의 Partition으로 나눠질 수 있습니다. Partition 내의 한칸은 로그라고 불리고 메시지는 Partition에 순차적으로 쌓이게 됩니다. 순차적으로 쌓이는 메시지들의 인덱스를 나타내는것이 바로 Offset입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 하나의 토픽의 메시지를 또 여러 개의 파티션으로 나누는 것일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 카프카는 메시지의 순서를 보장하지 않습니다. Partition과 Offset이 있는데 왜 순서를 보장할 수 없을까요? 왜그런지는 아래 메시지가 어떤 방식으로 저장되는지 알면 이해가 되실 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition이 하나만 있는 경우와 여러 개가 있는 경우를 생각해보면, 하나만 있다면 대용량의 메시지들이 발행된다면 저장하는데 꽤나 병목이 생길 것입니다. 하지만 여러 개가 있다면 병렬적으로 저장할 수 있어서 훨씬 수월하게 저장할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 카프카는 토픽으로 발행된 메시지는 라운드 로빈 방식으로 랜덤 Partition에 저장(물론 &lt;a href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=164096&quot;&gt;Partition Key&lt;/a&gt;를 이용하면 특정 파티션에 메시지를 적재 가능)되는데 해당 메시지들은 큐처럼 쌓이는 방식으로 저장됩니다. 그래서 특정 파티션에 들어온 메시지들은 Broker를 통해 Offset이라는 값이 계속 증가하는 인덱스를 부여받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Offset을 이용해서 Consumer는 해당 Topic내의 Partition의 메시지를 어디까지 읽었는지 북마크하여 Consumer가 죽었다 살아나도 다시 북마크된 부분부터 읽을 수가 있습니다. (장애 복구)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  위 내용을 정리하자면, 특정 Topic으로 들어온 메시지들은 Topic 내부의 파티션에 나눠서 저장되게 되고 파티션 내부에는 Offset이라는 정보를 통해 순서를 보장하지만, 파티션 간에는 보장하지 않습니다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세그먼트 파일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션에 추가되는 메시지들을 로그 파일이라고 하는데 이 파일들은 실제 파일 시스템에 저장이 됩니다. 이때 만들어지는 파일이 바로 세그먼트 파일이고 브로커 설정을 통해 세그먼트 파일이 일정 용량이 초과되거나 시간이 지나게 되면 삭제 또는 압축을 할 수 있습니다. 이때 파일은 세그먼트 단위로 삭제됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Consumer Group: 메시지를 소비하는 방법&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 2.png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rKSwd/btsGXq42k0H/3cOplEbZm0AS0HTSJlKFbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rKSwd/btsGXq42k0H/3cOplEbZm0AS0HTSJlKFbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rKSwd/btsGXq42k0H/3cOplEbZm0AS0HTSJlKFbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrKSwd%2FbtsGXq42k0H%2F3cOplEbZm0AS0HTSJlKFbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;564&quot; data-filename=&quot;Untitled 2.png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer는 그룹이라는 개념이 있는데 특정 토픽을 소비하는 컨슈머들에게 Group-Id를 부여하고 그룹으로 묶은 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 파티션은 Consumer Group내의 하나의 컨슈머에 의해서만 메시지가 소비되는 것을 보장합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;case 1) Partition 3개 - Consumer 2개&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 3.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;1366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2XfwZ/btsGXtUZlpk/6lgxY6ig0dTZBWNkLeXvs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2XfwZ/btsGXtUZlpk/6lgxY6ig0dTZBWNkLeXvs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2XfwZ/btsGXtUZlpk/6lgxY6ig0dTZBWNkLeXvs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2XfwZ%2FbtsGXtUZlpk%2F6lgxY6ig0dTZBWNkLeXvs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2168&quot; height=&quot;1366&quot; data-filename=&quot;Untitled 3.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;1366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티션이 3개, 컨슈머가 2개일 경우 &amp;rarr; 컨슈머 중 하나는 두개의 파티션을 소비&lt;/li&gt;
&lt;li&gt;이런 경우 처리량이 많아질수록 컨슈머에 병목현상이 발생할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;case 2) Partition 3개 - Consumer 3개&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 4.png&quot; data-origin-width=&quot;2176&quot; data-origin-height=&quot;1338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8fuZ1/btsGYA6TZD0/O1Aajcke87GskBUM5wdnNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8fuZ1/btsGYA6TZD0/O1Aajcke87GskBUM5wdnNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8fuZ1/btsGYA6TZD0/O1Aajcke87GskBUM5wdnNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8fuZ1%2FbtsGYA6TZD0%2FO1Aajcke87GskBUM5wdnNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2176&quot; height=&quot;1338&quot; data-filename=&quot;Untitled 4.png&quot; data-origin-width=&quot;2176&quot; data-origin-height=&quot;1338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티션의 개수와 컨슈머의 개수가 동일하다면 &amp;rarr; 파티션과 컨슈머가 1:1 매칭되어 메시지 소비&lt;/li&gt;
&lt;li&gt;가장 이상적인 구성이라고 할 수 있습니다. 파티션 당 하나의 컨슈머가 붙어서 빠르게 메시지를 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;case 3) Partition 3개 - Consumer 4개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 토픽의 파티션이 3개이고 그룹내의 컨슈머도 3개인 경우에 여기에 하나를 더 추가한다면 어떻게 될까요? 총 4개의 컨슈머니까 처리 속도가 훨씬 빨라질 수 있을까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 5.png&quot; data-origin-width=&quot;2468&quot; data-origin-height=&quot;1332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR3gyb/btsGXRH4TlZ/S54ZATyWiMr6OMgDNK0HJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR3gyb/btsGXRH4TlZ/S54ZATyWiMr6OMgDNK0HJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR3gyb/btsGXRH4TlZ/S54ZATyWiMr6OMgDNK0HJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR3gyb%2FbtsGXRH4TlZ%2FS54ZATyWiMr6OMgDNK0HJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2468&quot; height=&quot;1332&quot; data-filename=&quot;Untitled 5.png&quot; data-origin-width=&quot;2468&quot; data-origin-height=&quot;1332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 모든 파티션이 그룹내의 컨슈머와 매칭이 된 상황에 컨슈머를 하나 추가한 상황입니다. 파티션과 그룹 내 컨슈머는 1:1 매칭이 기본입니다. 그렇기 때문에 추가된 컨슈머는 자신에게 파티션이 배정되기를 기다리게 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 자원 낭비가 될 수도 있지만 메시지를 실시간으로 놓치면 안되는 서비스 관점에서 바라본다면 다른 컨슈머의 장애를 즉시 대비할 수 있는 좋은 전략이 될 수 있습니다. 일반적으로는 파티션 개수와 동일하게 컨슈머를 구성하는게 권장됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Broker와 Zoopkeeper란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 6.png&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baVKN6/btsGYOcW4gw/5bYzxdWtye0Gacui2ytmUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baVKN6/btsGYOcW4gw/5bYzxdWtye0Gacui2ytmUk/img.png&quot; data-alt=&quot;https://double.cloud/blog/posts/2023/03/the-many-use-cases-of-apache-kafka/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baVKN6/btsGYOcW4gw/5bYzxdWtye0Gacui2ytmUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaVKN6%2FbtsGYOcW4gw%2F5bYzxdWtye0Gacui2ytmUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1632&quot; height=&quot;1000&quot; data-filename=&quot;Untitled 6.png&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://double.cloud/blog/posts/2023/03/the-many-use-cases-of-apache-kafka/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커는 프로듀서와 컨슈머 간 메시지를 관리해주는 중간 관리자 역할을 합니다. 카프카 클러스터에는 여러 개의 브로커들로 구성되어 있는데요. 최소 3대 이상의 브로커들로 구성하는 것을 권장합니다. 주키퍼는 카프카 브로커들의 메타데이터들을 관리해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Broker는 무슨일을 할까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Leader Broker
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 토픽 파티션마다 존재하는 리더 브로커가 존재합니다. 리더 브로커는 0개 이상의 팔로워 브로커를 가집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Follwer Boker
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리더의 백업을 위해 존재하는 팔로워 브로커는 리더 브로커를 복제합니다.&lt;/li&gt;
&lt;li&gt;리더 브로커가 다운되면 Zookeeper에 의해서 팔로워 브로커 중 하나가 새로운 리더가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Controller
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클러스터의 다수 브로커 중 한대는 컨트롤러의 역할을 합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리더 선정, 토픽 생성, 파티션 생성, 복제본 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다른 브로커들의 상태를 체크하면서 브로커가 클러스터에서 빠지는 경우 해당 브로커에 존재하는 파티션들을 리밸런싱 하는 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cordinator
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 컨슈머 그룹에서 해당 토픽의 파티션에 대해서 1대1로 consume하고 있다고 가정했을 때, 컨슈머 하나가 장애가 발생했을 때, 다른 컨슈머에게 해당 &lt;b&gt;파티션을&lt;/b&gt; &lt;b&gt;Rebalance&lt;/b&gt; 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파티션 오프셋 관리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 파티션으로 부터 데이터를 가져가면 해당 파티션의 오프셋을 커밋하고 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zookeeper는 무슨일을 할까?&lt;/h3&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;Controller 선정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller 장애 발생 시 새로운 Controller를 선출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Broker, Consumer 관리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Broker 메타데이터 관리, 상태 정보 기록&lt;/li&gt;
&lt;li&gt;새로운 Broker 추가&lt;/li&gt;
&lt;li&gt;Broker 장애 감지
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Leader Broker 장애 시 새로운 Leader 선출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Topic 메타데이터 관리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Partition 개수, 설정 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;변경사항(토픽을 생성 또는 제거, 브로커 추가 또는 제거)들을 카프카 브로커들에게 알리는 역할&lt;/li&gt;
&lt;li&gt;주키퍼는 최소 3대 이상(엔터프라이즈는 5대 이상)을 권장하고 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주키퍼 앙상블: 주키퍼 3대 이상으로 구성한 것을 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@umanking/%EC%B9%B4%ED%94%84%EC%B9%B4%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0-%ED%95%98%EA%B8%B0%EC%A0%84%EC%97%90-%EB%A8%BC%EC%A0%80-data%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0%ED%95%B4%EB%B3%B4%EC%9E%90-d2e3ca2f3c2&quot;&gt;Kafka 이해하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@holicme7/Apache-Kafka-%EC%B9%B4%ED%94%84%EC%B9%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80&quot;&gt;[Apache Kafka] 카프카란 무엇인가?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://goodlucknua.tistory.com/120&quot;&gt;Kafka에 대해서 알아보자&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@greg.shiny82/apache-kafka-%EA%B0%84%EB%9E%B5%ED%95%98%EA%B2%8C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-343ad84a959b&quot;&gt;Apache Kafka 간략하게 살펴보기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jinseong-dev.tistory.com/42&quot;&gt;[Kafka] 넓고 얕게 카프카를 이해해보자&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://goodlucknua.tistory.com/120&quot;&gt;Kafka에 대해서 알아보자&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.redhat.com/ko/topics/integration/what-is-apache-kafka&quot;&gt;Apache Kafka(아파치 카프카)란? 소개, 생성, 설치 및 성능&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wonyong-jang.github.io/kafka/2021/02/09/Kafka-basic-concept.html&quot;&gt;[Kafka] Apache Kafka 이해하기 - SW Developer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nasa1515.com/apache-kafka-kafka/&quot;&gt;Apache Kafka란 무엇일까? [Kafka의 구조와 기초개념]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ggop-n.tistory.com/89&quot;&gt;[Kafka] Kafka 의 Topic 과 Partition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.scaler.com/topics/kafka-tutorial/kafka-partitions/&quot;&gt;Apache Kafka Topics, Partitions, and Offsets - Scaler Topics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>⚙️ Devops/  Kafka</category>
      <category>consumer group</category>
      <category>Kafka Broker</category>
      <category>kafka cluster</category>
      <category>OFFSET</category>
      <category>partition</category>
      <category>segment file</category>
      <category>topic</category>
      <category>zookeeper</category>
      <category>브로커</category>
      <category>주키퍼</category>
      <author>iseunghan</author>
      <guid isPermaLink="true">https://iseunghan.tistory.com/481</guid>
      <comments>https://iseunghan.tistory.com/481#entry481comment</comments>
      <pubDate>Fri, 26 Apr 2024 16:35:18 +0900</pubDate>
    </item>
    <item>
      <title>JPA - Fetch Join이 과연 만능인가? (N+1, Pagination)</title>
      <link>https://iseunghan.tistory.com/478</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가기 전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 시간에 알아봤던 N+1 해결법에 이어서 FetchJoin을 이용해서 해결할 수 있었습니다. 하지만 Fetch Join이라고 다 해결할 수 있는 것은 아닙니다. 이번 시간에는 Fetch Join을 사용했을 때 어떠한 사이드 이펙트가 있는지 알아보고 그 해결책에 대해서 알아봅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. FetchJoin, EntityGraph 사용 시 Pagination을 사용할 수 없다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FetchJoin과 EntityGraph 둘 다 동일한 증상이 발생합니다. Fetch Join만 테스트를 해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;@Query(
        value = &quot;select t from Team t join fetch t.members&quot;,
        countQuery = &quot;select count(t) from Team t&quot;
)
List&amp;lt;Team&amp;gt; findTeamsFetchJoin(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@DisplayName(&quot;모든 팀을 할 때, 페이지네이션이 안된다.&quot;)
@Test
void team_findAll_Pagination_test() {
    clearPersistenceContext();

    System.out.println(&quot;----------team_findAll_test start-----------&quot;);
    List&amp;lt;Team&amp;gt; teamList = teamRepository.findTeamsFetchJoin(PageRequest.of(0, 1));
    assertThat(teamList).hasSize(1);
    System.out.println(&quot;----------team_findAll_test mid-----------&quot;);
    teamList.stream()
            .map(Team::getMembers)
            .map(List::stream)
            .forEach(memberStream -&amp;gt; memberStream
                    .map(Member::getName)
                    .forEach(System.out::println)
            );
    System.out.println(&quot;----------team_findAll_test end-----------&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsVmo8/btsGLFs2XSs/EyT8PQg15Qatd0F1nbUr4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsVmo8/btsGLFs2XSs/EyT8PQg15Qatd0F1nbUr4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsVmo8/btsGLFs2XSs/EyT8PQg15Qatd0F1nbUr4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsVmo8%2FbtsGLFs2XSs%2FEyT8PQg15Qatd0F1nbUr4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;662&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 살펴보면 LIMIT 쿼리가 발생하지 않고, &lt;code&gt;firstResult/maxResults specified with collection fetch; applying in memory&lt;/code&gt; 라는 경고가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 즉슨, Full Scan해서 전부 다 들고와서 애플리케이션 단에서 메모리 상에 올려두고 페이지네이션 처리를 했다는 뜻입니다. 만약 몇천만건이라면 어떻게 될까요? OOM(OutOfMemory)이 발생해서 장애가 발생할 가능성이 매우 큽니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 어떻게 해결해야 할까요? 메모리 상에 올려두고 Pagination을 한다는점은 너무나 치명적이기 때문에 N+1의 이점을 포기하는게 맞다고 생각합니다. 하지만 이건 상황에 따라 달라질 수 있을 것 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;만약 Pagination을 사용하지 않는다면, FetchJoin or EntityGraph로 N+1 해결&lt;/li&gt;
&lt;li&gt;만약 Pagination을 중요시 한다면, N+1 포기 (하지만 Batch Size를 이용한다면 N+1에 대해 최소한의 성능을 보장시킬 수 있습니다)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에서는 후자의 방법을 살펴봅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결방법 - Fetch Join 제거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 Fetch Join을 포기해야하는 것은 불가피합니다. 그렇기 때문에 findAll(Pageable) 메소드를 이용하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;puppet&quot;&gt;&lt;code&gt;/**
 * Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
 *
 * @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
 *          {@literal null}.
 * @return a page of entities
 */
Page&amp;lt;T&amp;gt; findAll(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@DisplayName(&quot;모든 팀을 조회할 때, 기본제공 findAll + Pageable은 Limit 쿼리가 발생한다.&quot;)
@Test
void team_default_findAll_Pagination_test() {
    clearPersistenceContext();

    System.out.println(&quot;----------team_findAll_test start-----------&quot;);
    Page&amp;lt;Team&amp;gt; result = teamRepository.findAll(PageRequest.of(0, 1));
    List&amp;lt;Team&amp;gt; teamList = result.getContent();
    assertThat(teamList).hasSize(1);
    System.out.println(&quot;----------team_findAll_test mid-----------&quot;);
    teamList.stream()
            .map(Team::getMembers)
            .map(List::stream)
            .forEach(memberStream -&amp;gt; memberStream
                    .map(Member::getName)
                    .forEach(System.out::println)
            );
    System.out.println(&quot;----------team_findAll_test end-----------&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2062&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ph9Id/btsGIMOvyZc/IAkee2YjvA0oJtopqtTN2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ph9Id/btsGIMOvyZc/IAkee2YjvA0oJtopqtTN2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ph9Id/btsGIMOvyZc/IAkee2YjvA0oJtopqtTN2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fph9Id%2FbtsGIMOvyZc%2FIAkee2YjvA0oJtopqtTN2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;530&quot; height=&quot;546&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2062&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 정상적으로 offset 쿼리를 통해 pagination을 처리하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 fetch join을 제거한 @Query를 사용하면 Pagination이 잘 동작하는지 볼까요?&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;@Query(
        value = &quot;select t from Team t&quot;,
        countQuery = &quot;select count(t) from Team t&quot;
)
List&amp;lt;Team&amp;gt; findTeamsWithoutFetchJoin(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@DisplayName(&quot;모든 팀을 조회할 때, Fetch Join 제거 + Pageable은 Limit 쿼리가 발생한다.&quot;)
@Test
void team_findAll_Exclusive_FetchJoin_with_Pagination_test() {
    clearPersistenceContext();

    System.out.println(&quot;----------team_findAll_test start-----------&quot;);
    Page&amp;lt;Team&amp;gt; result = teamRepository.findTeamsWithoutFetchJoin(PageRequest.of(0, 1));
    List&amp;lt;Team&amp;gt; teamList = result.getContent();
    assertThat(teamList).hasSize(1);
    System.out.println(&quot;----------team_findAll_test mid-----------&quot;);
    teamList.stream()
            .map(Team::getMembers)
            .map(List::stream)
            .forEach(memberStream -&amp;gt; memberStream
                    .map(Member::getName)
                    .forEach(System.out::println)
            );
    System.out.println(&quot;----------team_findAll_test end-----------&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mw4Gd/btsGKMsZG4n/joEdYfhkrPkwbKykmLOL1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mw4Gd/btsGKMsZG4n/joEdYfhkrPkwbKykmLOL1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mw4Gd/btsGKMsZG4n/joEdYfhkrPkwbKykmLOL1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMw4Gd%2FbtsGKMsZG4n%2FjoEdYfhkrPkwbKykmLOL1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;718&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2521&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 만든 쿼리도 정상적으로 Pagination이 동작하는 것을 확인할 수 있습니다. (추가 팁: Batch Size를 이용한다면 N+1에 대해 최소한의 성능을 보장시킬 수 있습니다)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Limit 결정되는 시점&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;851&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JiIHL/btsGJliDaMX/WGlmUaYhIJWW0kDQBB6k90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JiIHL/btsGJliDaMX/WGlmUaYhIJWW0kDQBB6k90/img.png&quot; data-alt=&quot;SimpleJpaRepository - 692 line&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JiIHL/btsGJliDaMX/WGlmUaYhIJWW0kDQBB6k90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJiIHL%2FbtsGJliDaMX%2FWGlmUaYhIJWW0kDQBB6k90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;851&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;851&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SimpleJpaRepository - 692 line&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleJpaRepository에서 페이지 요청이 있으면 offSet과 MaxResults를 지정해주고 있습니다. 그 정보를 바탕으로 실제 쿼리를 날릴 때 Offset과 First or Top을 이용해서 Limit를 구현합니다. (자세한 글은 다음을 &lt;a href=&quot;https://stackoverflow.com/questions/44565820/what-is-the-limit-clause-alternative-in-jpql&quot;&gt;참조&lt;/a&gt;)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;번외 - &lt;code&gt;~ToOne&lt;/code&gt; 관계에서는 페이징 처리가 가능하다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 Fetch Join + Pagination을 사용하면 안되는 케이스들은 모두 &lt;code&gt;~ToMany&lt;/code&gt; 관계일 때 입니다. 아래에서 소개드리는 것은 근본적인 해결방법은 아니지만, &lt;code&gt;~ToOne&lt;/code&gt; 관계에서는 페이징 처리가 가능하다는 것을 테스트하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Query(value = &quot;select m from Member m join fetch m.team&quot;)
List&amp;lt;Member&amp;gt; findMembersFetchJoin(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@DisplayName(&quot;~ToOne관계에서는 페이징처리가 가능하다&quot;)
@Test
void member_findAll_Pagination_test() {
    clearPersistenceContext();

    System.out.println(&quot;----------member_findAll_test start-----------&quot;);
    List&amp;lt;Member&amp;gt; memberList = memberRepository.findMembersFetchJoin(PageRequest.of(0, 2));
    assertThat(memberList).hasSize(2);
    System.out.println(&quot;----------member_findAll_test mid-----------&quot;);
    memberList.stream()
            .map(Member::getTeam)
            .map(Team::getName)
            .forEach(System.out::println);
    System.out.println(&quot;----------member_findAll_test end-----------&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwKh9Y/btsGJO5TrfC/tWVuGSdcJuHQMhFaAMltk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwKh9Y/btsGJO5TrfC/tWVuGSdcJuHQMhFaAMltk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwKh9Y/btsGJO5TrfC/tWVuGSdcJuHQMhFaAMltk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwKh9Y%2FbtsGJO5TrfC%2FtWVuGSdcJuHQMhFaAMltk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;576&quot; height=&quot;442&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보시는 것과 같이 &lt;code&gt;~ToOne&lt;/code&gt; 관계에서는 문제 없이 조회되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. MultipleBagFetchException -&lt;/b&gt; 하나의 엔티티에서 2개 이상의 컬렉션을 조회할 때 (&lt;code&gt;~ToMany&lt;/code&gt; 관계)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RHUK1/btsGKOdhltV/GpRJUQlKsQpgI2KXgg6m31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RHUK1/btsGKOdhltV/GpRJUQlKsQpgI2KXgg6m31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RHUK1/btsGKOdhltV/GpRJUQlKsQpgI2KXgg6m31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRHUK1%2FbtsGKOdhltV%2FGpRJUQlKsQpgI2KXgg6m31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1086&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 ERD 처럼 Team이 members, sponsors를 갖는 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Team {
        ...

    @OneToMany(mappedBy = &quot;team&quot;, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();

    @OneToMany(mappedBy = &quot;team&quot;, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List&amp;lt;Sponsor&amp;gt; sponsors = new ArrayList&amp;lt;&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface TeamRepository extends JpaRepository&amp;lt;Team, Long&amp;gt; {
    ...

    @Query(value = &quot;select t from Team t join fetch t.members join fetch t.sponsors&quot;)
    Page&amp;lt;Team&amp;gt; findTeamsFetchJoinTwoCollection(Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@DisplayName(&quot;둘 이상의 컬렉션을 페치조인할 수 없다.&quot;)
@Test
void Over_Two_collection_fetchJoin_Not_Allowed() throws Exception {
    System.out.println(&quot;--------Over_Two_collection_fetchJoin_Not_Allowed START-------------&quot;);
    // when
    List&amp;lt;Team&amp;gt; result = teamRepository.findTeamsFetchJoinTwoCollection(PageRequest.of(0, 3)).getContent();
    assertThat(teams).hasSize(3);

    // then
    System.out.println(&quot;--------Over_Two_collection_fetchJoin_Not_Allowed MID-------------&quot;);
    teams.forEach(team -&amp;gt; {
        System.out.println(team.getName());
        team.getMembers().stream().map(Member::getName).forEach(System.out::println);
        team.getSponsors().stream().map(Sponsor::getName).forEach(System.out::println);
    });

    System.out.println(&quot;--------Over_Two_collection_fetchJoin_Not_Allowed END-------------&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tDgnz/btsGJlCSGst/USElkqB17kpuJ9uQyTaER0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tDgnz/btsGJlCSGst/USElkqB17kpuJ9uQyTaER0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tDgnz/btsGJlCSGst/USElkqB17kpuJ9uQyTaER0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtDgnz%2FbtsGJlCSGst%2FUSElkqB17kpuJ9uQyTaER0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;532&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 이상의 컬렉션을 Fetch Join 하려니 &lt;code&gt;cannot simultaneously fetch multiple bags&lt;/code&gt; 에러가 발생합니다. 어떻게 하면 MultipleBagFetchException을 피할 수 있을까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결방법 1. Set 자료형으로 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 이상의 컬렉션을 조회하면 컬렉션X컬렉션 즉 카테시안곱이 발생하기 때문에 JPA에서 예외를 던지게 했습니다. 그래서 중복이 발생하지 않기 위해 Set 자료형으로 변경하면 해결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@OneToMany(mappedBy = &quot;team&quot;, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set&amp;lt;Member&amp;gt; members = new LinkedHashSet&amp;lt;&amp;gt;();

@OneToMany(mappedBy = &quot;team&quot;, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set&amp;lt;Sponsor&amp;gt; sponsors = new LinkedHashSet&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서를 보장해주기 위해 LinkedHashSet을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;@Query(value = &quot;select t from Team t join fetch t.members join fetch t.sponsors&quot;)
List&amp;lt;Team&amp;gt; findTeamsFetchJoinTwoCollection();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt4d3Y/btsGI7dOhgO/bXE2bkEVOZbF1j2cBqxjPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt4d3Y/btsGI7dOhgO/bXE2bkEVOZbF1j2cBqxjPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt4d3Y/btsGI7dOhgO/bXE2bkEVOZbF1j2cBqxjPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt4d3Y%2FbtsGI7dOhgO%2FbXE2bkEVOZbF1j2cBqxjPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;627&quot; height=&quot;529&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1687&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 테스트를 돌려보면 정상적으로 Fetch Join이 수행하여 N+1 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할점이 있습니다. Fetch Join은 기본적으로 &lt;code&gt;Inner Join&lt;/code&gt;을 사용합니다. 그렇기 때문에 만약 sponsor나 member를 가지고 있지 않는 team이 있다면? 아마 테스트를 깨지게 될 것입니다. 한번 확인해볼까요?&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@BeforeEach
void setup() {
        ...

    Sponsor sponsor21 = Sponsor.builder().name(&quot;sponsor21&quot;).build();
    Sponsor sponsor22 = Sponsor.builder().name(&quot;sponsor22&quot;).build();
    Team team2 = Team.builder().name(&quot;team2&quot;).build();
    team2.addMember(Member.builder().name(&quot;member2-1&quot;).build());
    team2.addMember(Member.builder().name(&quot;member2-2&quot;).build());
//    team2.addSponsor(sponsor21);
//    team2.addSponsor(sponsor22);

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트용 데이터를 삽입할 때, 일부러 team2의 sponsor를 추가하는 코드를 주석처리하고 테스트를 돌려보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;523&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mHOxD/btsGJlXcRyy/WPhgyPfZjCLQdGizkVpXU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mHOxD/btsGJlXcRyy/WPhgyPfZjCLQdGizkVpXU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mHOxD/btsGJlXcRyy/WPhgyPfZjCLQdGizkVpXU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmHOxD%2FbtsGJlXcRyy%2FWPhgyPfZjCLQdGizkVpXU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;523&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;523&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 깨지게 됩니다. 이는 어찌보면 당연한 결과입니다. Inner Join을 이용해서 Team을 조회해오기 때문에 해당되지 않는다면 조회가 되지 않을 것 입니다. 만약 member, sponsor가 없는 Team도 가져와야 한다면 inner join이 아닌 left join을 사용하면 될 것입니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;@Query(value = &quot;select t from Team t left join fetch t.members left join fetch t.sponsors&quot;)
Page&amp;lt;Team&amp;gt; findTeamsFetchJoinTwoCollection(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi3DYE/btsGI5fZyfB/xkK3bYRaQRcZy4nSk2dCCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi3DYE/btsGI5fZyfB/xkK3bYRaQRcZy4nSk2dCCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi3DYE/btsGI5fZyfB/xkK3bYRaQRcZy4nSk2dCCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi3DYE%2FbtsGI5fZyfB%2FxkK3bYRaQRcZy4nSk2dCCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;582&quot; height=&quot;470&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1615&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;left join을 통해서 모든 팀을 다 가져오는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과연 이 방법이 최선일까요? 위 방법에는 Pagination을 In Memory에서 한다는 점이 아직 해결되지 않았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결방법 2. BatchSize&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 에러를 해결하기 위해 도메인 레이어의 자료형을 변경하는 것은 좋은 해결책이 아니라고 생각합니다. 그렇기 때문에 List 자료형을 사용해야하고 Pagination, 둘 이상의 컬렉션을 Fetch Join을 한다는 가정하에 Batch Size를 이용해 해결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(properties = &quot;spring.jpa.properties.hibernate.default_batch_fetch_size=10&quot;)
public class Team_MultipleBagEx_Solution2_Test {
        ...

        @DisplayName(&quot;둘 이상의 컬렉션을 페치조인할 수 없다 -&amp;gt; Fetch Join(x) + BatchSize를 이용해 해결&quot;)
        @Test
        void Over_Two_collection_Not_Used_fetchJoin_and_Use_BatchSize() throws Exception {
            System.out.println(&quot;--------Over_Two_collection_Not_Used_fetchJoin_and_Use_BatchSize START-------------&quot;);
            // when
            List&amp;lt;Team&amp;gt; teams = teamRepository.findAll(PageRequest.of(0, 3)).getContent();
            assertThat(teams).hasSize(3);

            // then
            System.out.println(&quot;--------Over_Two_collection_Not_Used_fetchJoin_and_Use_BatchSize MID-------------&quot;);
            teams.forEach(team -&amp;gt; {
                System.out.println(team.getName());
                team.getMembers().stream().map(Member::getName).forEach(System.out::println);
                team.getSponsors().stream().map(Sponsor::getName).forEach(System.out::println);
            });

            System.out.println(&quot;--------Over_Two_collection_Not_Used_fetchJoin_and_Use_BatchSize END-------------&quot;);
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@TestPropertySource(properties = &quot;spring.jpa.properties.hibernate.default_batch_fetch_size=10&quot;)&lt;/code&gt; 를 이용해서 해당 테스트에 BatchSize를 10으로 설정하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sUWwp/btsGJq5cOQa/04k6dtpk7Ki5KGYCUApNQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sUWwp/btsGJq5cOQa/04k6dtpk7Ki5KGYCUApNQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sUWwp/btsGJq5cOQa/04k6dtpk7Ki5KGYCUApNQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsUWwp%2FbtsGJq5cOQa%2F04k6dtpk7Ki5KGYCUApNQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;581&quot; height=&quot;647&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 살펴보면, Paging이 잘 처리되었습니다. 그리고 Team에 속한 member, sponsor를 사용할 때 쿼리가 발생하였고, &lt;code&gt;where in&lt;/code&gt; 절을 통해 Eager Loading을 하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 &lt;a href=&quot;https://github.com/iseunghan/iseunghan-Lab/pull/2/commits/887641008ac8067dfb2adf3ade00879173d1d884#diff-77a41d663525f7442a52ea219e98dc2efbccfce4816d70b99f3cf237b7a5738aR176-R218&quot;&gt;BatchSize를 적용하지 않고 테스트를 돌렸다면&lt;/a&gt; 몇번의 쿼리가 발생했을까요? 전체 팀의 개수가 3이라면 4의 쿼리가 발생했을 것입니다.(N+1 문제)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4개의 쿼리가 3개로 줄어들었는데 1개밖에 차이가 안나네? 라고 생각하실 수 있습니다. 하지만 아래 표와 같이 팀의 개수가 많을수록 쿼리 수는 눈에 띄게 줄어듭니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 112px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 40px;&quot;&gt;전체 팀의 개수&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 40px;&quot;&gt;발생 쿼리 수&lt;br /&gt;(BatchSize 미적용)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 40px;&quot;&gt;발생 쿼리 수&lt;br /&gt;(BatchSize=1000 적용)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 40px;&quot;&gt;쿼리 성능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;25% 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;1,000&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;1,001&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;99.7% 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;10,000&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;10,001&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;21&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;99.8% 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;100,000&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;100,001&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;201&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;99.8% 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대 1000 분의 1로 쿼리수를 줄일 수 있습니다. Fetch Join을 사용할 수 있다면 Fetch Join을 사용하고, 만약 둘 이상의 컬렉션을 조회해야한다면 가장 많은 컬렉션에 Fetch Join을 적용하고 나머지 컬렉션에 대해서는 Batch Size를 사용하는 방법이 최선일 것 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. FetchJoin 대상은 별칭을 사용하지 말자.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 표준 스펙에는 Fetch Join 대상에 별칭이 없습니다. 하지만 JPA 구현체인 하이버네이트는 별칭을 허용합니다. 즉 별칭을 사용을 할 수 있지만 여러 문제점들이 있기 때문에 조심해서 사용해야합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fetch Join 대상을 ON절에서 사용하면 에러가 발생한다.&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@DisplayName(&quot;join fetch는 on절 사용시 org.hibernate.query.SemanticException: Fetch join has a 'with' clause (use a filter instead) 발생&quot;)
@Test
void fetchJoin_NotAllow_On_condition() throws Exception {
    System.out.println(&quot;--------fetchJoin_NotAllow_On_condition START-------------&quot;);
    // when
    List&amp;lt;Team&amp;gt; teams = em.createQuery(&quot;select t from Team t join fetch t.members m on m.name = 'member11'&quot;, Team.class).getResultList();

    // then
    System.out.println(&quot;--------fetchJoin_NotAllow_On_condition MID-------------&quot;);
    teams.forEach(team -&amp;gt; {
        team.getMembers().stream().map(Member::getName).forEach(System.out::println);
        team.getSponsors().stream().map(Sponsor::getName).forEach(System.out::println);
    });
    System.out.println(&quot;--------fetchJoin_NotAllow_On_condition END-------------&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ct9kew/btsGK8oU4or/XROidDBfk6bGlx0E7ywSeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ct9kew/btsGK8oU4or/XROidDBfk6bGlx0E7ywSeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ct9kew/btsGK8oU4or/XROidDBfk6bGlx0E7ywSeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fct9kew%2FbtsGK8oU4or%2FXROidDBfk6bGlx0E7ywSeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;526&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 테스트를 실행하면 on절 대신에 filter를 사용하라고 에러를 발생시킵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼 where 절에 조건을 걸면 안되나?&lt;/h3&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;Team을 조회하는데 해당 Team에 속한 모든 Member도 필요한 상황&lt;/li&gt;
&lt;li&gt;쿼리 최적화를 위해 Team.members에 대해서 Fetch Join 적용&lt;/li&gt;
&lt;li&gt;이름이 &amp;ldquo;member11&amp;rdquo;로 시작하는 member를 가져와야함&lt;/li&gt;
&lt;li&gt;위 요구사항으로 만들어진 쿼리는 다음과 같음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;select t from Team t join fetch t.members m where m.name like 'member1%'&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 team에 속한 member가 member1, member12, member2라고 가정했을 때 위 결과는 member1, member12가 나올 것입니다. 이렇게 되면 JPA 입장에서는 DB와 데이터 일관성이 깨지게 되고, 최악의 경우 member2가 DB에서 삭제될 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면 JPA의 엔티티는 DB의 데이터와 일관성을 유지해야 하기 때문에 임의로 데이터를 빼고 조회한다면 DB에 해당 데이터가 없다고 판단하는 것과 동일합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼 하이버네이트는 왜 허용할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 하이버네이트에서는 Fetch Join 대상의 별칭을 허용할까요? 만약 조회하는 데이터가 DB와의 일관성 문제가 없다면 사용해도 됩니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 쿼리는 안전하다고 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;select m 
from Member m 
    join fetch m.team t
where t.name = 'team1'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;join fetch 대상인 Team의 이름을 필터링해서 가져오는 쿼리입니다. 결론적으로 ~ToOne 관계인 컬렉션에 대해서 필터링하는 것은 안전하다고 할 수 있는거죠. 하지만 조회 용도로만 사용한다는 가정하에 말씀드리는 것입니다. 실무에서는 DB와 일관성이 깨지더라도, 조회 용도로만 사용한다면 Fetch Join 대상의 별칭을 사용하여도 됩니다. (대신 2차 캐시 등등 조심..)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리하자면..&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fetch Join
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이징 API를 사용할 수 없다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해결방법: 페이징 필요 시: Fetch Join 제거 + Batch Size, 불 필요 시: Fetch Join 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;둘 이상의 컬렉션을 Fetch Join 할 수 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해결방법: 데이터가 많은 쪽에 Fetch Join 걸고, 나머지는 Batch Size를 이용해 최소한의 성능 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Fetch Join 대상 ON, Where절 사용 조심
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해결방법: Fetch Join 시 조회 용도로만 사용한다면 OK, 아니라면 절대 금지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하다보니 내용이 너무 길어졌네요.. JPA는 정말 알아도 끝이 없는 것 같습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부족한 글 읽어주셔서 감사합니다.&lt;/p&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://jojoldu.tistory.com/457&quot;&gt;MultipleBagFetchException 발생시 해결 방법&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cheese10yun.github.io/jpa-nplus-1/&quot;&gt;JPA N+1 발생원인과 해결방법 - Yun Blog | 기술 블로그&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85#pagination-%ED%95%B4%EA%B2%B0%EC%B1%85-1--toone-%EA%B4%80%EA%B3%84%EC%97%90%EC%84%9C-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%B2%98%EB%A6%AC&quot;&gt;JPA 모든 N+1 발생 케이스과 해결책&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bottom-to-top.tistory.com/45&quot;&gt;fetch join 과 limit 을 같이 쓸 때 주의하자. (firstResult/maxResults specified with collection fetch)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/questions/15876/fetch-join-%EC%8B%9C-%EB%B3%84%EC%B9%AD%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4&quot;&gt;fetch join 시 별칭관련 질문입니다 - 인프런 | 질문 &amp;amp; 답변&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/questions/59632/fetch-join-%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8-%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4&quot;&gt;fetch join 관련 질문 드립니다!! - 인프런&lt;/a&gt;&lt;/p&gt;</description>
      <category>  JAVA/자바 ORM 표준 JPA 프로그래밍</category>
      <category>batch size</category>
      <category>fetch join</category>
      <category>join fetch</category>
      <category>JPA</category>
      <author>iseunghan</author>
      <guid isPermaLink="true">https://iseunghan.tistory.com/478</guid>
      <comments>https://iseunghan.tistory.com/478#entry478comment</comments>
      <pubDate>Thu, 18 Apr 2024 23:28:18 +0900</pubDate>
    </item>
    <item>
      <title>개발자가 알아야 할 UML 개념 및 종류 (feat. 협업 시 필수!)</title>
      <link>https://iseunghan.tistory.com/477</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;UML이란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;1999&quot; data-origin-height=&quot;1122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsO6kQ/btsFYL1MKJV/iwIPkDMdc0t270biz1P1p1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsO6kQ/btsFYL1MKJV/iwIPkDMdc0t270biz1P1p1/img.png&quot; data-alt=&quot;https://miro.com/blog/uml-diagram/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsO6kQ/btsFYL1MKJV/iwIPkDMdc0t270biz1P1p1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsO6kQ%2FbtsFYL1MKJV%2FiwIPkDMdc0t270biz1P1p1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1999&quot; height=&quot;1122&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;1999&quot; data-origin-height=&quot;1122&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://miro.com/blog/uml-diagram/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UML은 Unified Modeling Language(통합 모델링 언어)의 약자로 1997년, OMG(Object Management Group) 표준화 기구에서 모델을 만드는 &lt;b&gt;표준 언어&lt;/b&gt;로 채택되었습니다. UML은 객체 지향 소프트웨어를 개발할 때 시스템과 산출물을 &lt;b&gt;명세화, 시각화, 문서화&lt;/b&gt;할 때 사용합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UML을 사용하는 이유?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 아이디어와 시스템을 비개발자도 쉽게 이해&lt;/li&gt;
&lt;li&gt;시스템의 전체 구조를 이미지로 한눈에 쉽게 파악&lt;/li&gt;
&lt;li&gt;표준화된 기호를 이용하기 때문에 서로 다른 개발자와 소통 불일치 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어를 개발 또는 분석 설계를 위해 개발자, 기획자, 아키텍처 등이 참여를 합니다. 클라이언트의 요구사항을 해결하기 위한 기능에 대해서 코드로 작성하면 너무 길어지고 한번에 이해하기는 더더욱 힘듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기능에 대한 수많은 코드들을 UML을 이용해 시각화하면, 정보들이 단순화되어 이해하는데 쉽고 커뮤니케이션을 더 원할하게 해주는 장점이 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대표적인 다이어그램들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다이어그램은 크게 구조도 다이어그램과 행동 다이어그램, 두가지로 나뉘는데요. 해당 포스팅에서는 가장 많이 사용되는 3가지에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스 다이어그램: 클래스의 속성, 메서드, 관계를 표현합니다.&lt;/li&gt;
&lt;li&gt;유스케이스 다이어그램: 사용자(Actor)의 관점에서 시스템의 기능, 상호작용과 그들간의 관계를 표현합니다.&lt;/li&gt;
&lt;li&gt;시퀀스 다이어그램: 객체간의 상호작용을 시간의 흐름에 따라 나타냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 클래스 다이어그램 (Class Diagram)&lt;/h3&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;Coffee 클래스를 정의합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이름을 나타내는 &lt;code&gt;name: String&lt;/code&gt; 변수, 단맛을 나타내는 &lt;code&gt;Sweetnees: int&lt;/code&gt; 변수, 산미를 나타내는 &lt;code&gt;acidity: int&lt;/code&gt; 변수가 있습니다.&lt;/li&gt;
&lt;li&gt;함수의 이름은 make이고 리턴타입은 void인 함수를 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Coffee 클래스를 상속받는 &lt;code&gt;Americano&lt;/code&gt; 클래스를 정의합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;addShot&lt;/code&gt;이라는 함수의 리턴타입은 void인 함수를 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Coffee 클래스를 상속받는 &lt;code&gt;Vanila Latte&lt;/code&gt; 클래스를 정의합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;addShot&lt;/code&gt;이라는 함수의 리턴타입은 void인 함수를 정의합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addSyrup&lt;/code&gt; 이라는 함수의 리턴타입은 void인 함수를 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설명을 클래스 다이어그램으로 나타내보면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 1.png&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;852&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDzrMa/btsFW9CFe8u/jESUOXbcouKYcGfPwM3Am0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDzrMa/btsFW9CFe8u/jESUOXbcouKYcGfPwM3Am0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDzrMa/btsFW9CFe8u/jESUOXbcouKYcGfPwM3Am0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDzrMa%2FbtsFW9CFe8u%2FjESUOXbcouKYcGfPwM3Am0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;392&quot; height=&quot;416&quot; data-filename=&quot;Untitled 1.png&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;852&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 그림 하나로 설명을 할 수 있습니다. 어떤가요? 그림으로 그려서 보니까 훨씬 더 이해가 빠르지 않나요? 이렇게 다이어그램으로 협업하면 오해와 불필요한 소통은 줄이고 명확하게 소통을 할 수 있습니다. 하지만 클래스다이어그램은 함수의 내부구현까지는 알지 못합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 유스케이스 다이어그램 (Usecase Diagram)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유스 케이스 다이어그램은 사용자의 관점에서 어떤 행동을 하는지, 그리고 그 행동을 위해 어떤 과정이 있는지 그 과정 끝에 어떤 결과가 발생하는지에 대해서 그림으로 그려낸 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 사이트에 대한 유스케이스를 먼저 나열해보면 뭐가 있을까요?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자는 로그인을 할 수 있다.&lt;/li&gt;
&lt;li&gt;문서를 검색할 수 있다.&lt;/li&gt;
&lt;li&gt;검색한 문서를 미리보기할 수 있다.&lt;/li&gt;
&lt;li&gt;문서는 다운로드할 수 있다.&lt;/li&gt;
&lt;li&gt;진행중인 이벤트들을 볼 수 있다.&lt;/li&gt;
&lt;li&gt;사용자는 문서를 업로드할 수 있다.&lt;/li&gt;
&lt;li&gt;업로드한 문서들을 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;사용자를 추가할 수 있다.. 등등..&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 나열한 기능들을 유스케이스로 변환하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 2.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;731&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLCxGc/btsFU8Y01Fu/d1kxFI54RbNjw2zGOdgzak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLCxGc/btsFU8Y01Fu/d1kxFI54RbNjw2zGOdgzak/img.png&quot; data-alt=&quot;https://www.lucidchart.com/pages/uml-use-case-diagram&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLCxGc/btsFU8Y01Fu/d1kxFI54RbNjw2zGOdgzak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLCxGc%2FbtsFU8Y01Fu%2Fd1kxFI54RbNjw2zGOdgzak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;594&quot; height=&quot;579&quot; data-filename=&quot;Untitled 2.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;731&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.lucidchart.com/pages/uml-use-case-diagram&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유스케이스는 사용자 관점에서 어떤 행동들이 있고 그 행동들은 또 어떤 행동들과 연관이 있는지에 대해서 큰 틀을 이해하는데 최적화되어 있습니다. 하지만 그 행동에 어떤 정보가 오고가는지에 대해서는 자세하게 알 수 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 시퀀스 다이어그램 (Sequence Diagram)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스 다이어그램은 왼쪽 위에서 부터 오른쪽 아래로 순차적으로 흘러가는 다이어그램입니다. 데이터가 어떻게 흘러가는지와 결론적으로 어느곳으로 도달하는지에 대해서 한눈에 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 휴대폰으로 사진을 찍을 때 데이터의 흐름을 적어보겠습니다.&lt;/p&gt;
&lt;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;카메라 앱을 디바이스의 카메라에 접근합니다.&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;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled 3.png&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TlGpG/btsFU7FKcGQ/0eo1xR9K9xYrOd0PcbFDJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TlGpG/btsFU7FKcGQ/0eo1xR9K9xYrOd0PcbFDJ1/img.png&quot; data-alt=&quot;https://www.geeksforgeeks.org/unified-modeling-language-uml-sequence-diagrams/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TlGpG/btsFU7FKcGQ/0eo1xR9K9xYrOd0PcbFDJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTlGpG%2FbtsFU7FKcGQ%2F0eo1xR9K9xYrOd0PcbFDJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;500&quot; data-filename=&quot;Untitled 3.png&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.geeksforgeeks.org/unified-modeling-language-uml-sequence-diagrams/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스 다이어그램은 말 그대로 순서대로 흘러가는 그림이기 때문에 비개발자도 쉽게 보고 이해할 수 있습니다. 하나의 기능에 대해서 표현하기 때문에 전체 기능을 표현할 수 없다는 단점이 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Outro&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 소개해드린 것 말고 더 많은 다이어그램들이 존재합니다. 처음엔 UML로 그리기 매우 귀찮습니다.. UML을 그리다보면 &lt;code&gt;차라리 이 시간에 개발을 하는게 더 빠르지 않을까?&lt;/code&gt; 라는 생각이 들기도 하구요..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해보시면 다를 겁니다. 이런 문서들을 작성하지 않고 개발하다보면 어느 순간 길을 잃을 때가 많습니다. 글로 작성된 문서를 보면 전에는 명확하게 정의내릴 수 있었는데 시간이 지나다보면 인간은 망각의 동물이라고 잊어버리고 다시 그 가닥을 잡기위해 더 많은 시간을 투자해야하구요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴 땐 UML을 그려놓으면 한눈에 이해할 수 있어서 나중에 개발할 때 막히더라도 금방 이해하고 다시 개발에 전념할 수 있습니다! 개발자 기획자 간 원할한 협업을 위해서도 UML은 필수라고 생각합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 글 읽어주셔서 감사합니다. 다음 시간에는 더 많은 UML에 대해서 더 자세하게 다뤄보도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;figure id=&quot;og_1710942761706&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;개발자들의 의사소통을 위한 언어, UML 알아보기 | 요즘IT&quot; data-og-description=&quot;UML은 개발자와 개발 프로젝트를 위한 시각적 도구입니다. 마치 우리가 조별과제를 하면서 서로 소통에 문제가 생기거나, 잘못된 결과를 만들기도 하듯이 개발자들이 함께 모여 작업을 할 때는 &quot; data-og-host=&quot;yozm.wishket.com&quot; data-og-source-url=&quot;https://yozm.wishket.com/magazine/detail/629/&quot; data-og-url=&quot;https://yozm.wishket.com/magazine/detail/629/yozm.wishket.com/magazine/detail/629/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gE8Cs/hyVAM6Jmuq/8MtQkDcsR1rmmAvFv9IRKK/img.png?width=588&amp;amp;height=470&amp;amp;face=0_0_588_470,https://scrap.kakaocdn.net/dn/ty4EL/hyVDIn5iPT/dBMuwk7H1nLDLckoXojCqK/img.png?width=588&amp;amp;height=470&amp;amp;face=0_0_588_470&quot;&gt;&lt;a href=&quot;https://yozm.wishket.com/magazine/detail/629/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yozm.wishket.com/magazine/detail/629/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gE8Cs/hyVAM6Jmuq/8MtQkDcsR1rmmAvFv9IRKK/img.png?width=588&amp;amp;height=470&amp;amp;face=0_0_588_470,https://scrap.kakaocdn.net/dn/ty4EL/hyVDIn5iPT/dBMuwk7H1nLDLckoXojCqK/img.png?width=588&amp;amp;height=470&amp;amp;face=0_0_588_470');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;개발자들의 의사소통을 위한 언어, UML 알아보기 | 요즘IT&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;UML은 개발자와 개발 프로젝트를 위한 시각적 도구입니다. 마치 우리가 조별과제를 하면서 서로 소통에 문제가 생기거나, 잘못된 결과를 만들기도 하듯이 개발자들이 함께 모여 작업을 할 때는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yozm.wishket.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1710942760631&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Sequence Diagrams | Unified Modeling Language (UML) - GeeksforGeeks&quot; data-og-description=&quot;A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.&quot; data-og-host=&quot;www.geeksforgeeks.org&quot; data-og-source-url=&quot;https://www.geeksforgeeks.org/unified-modeling-language-uml-sequence-diagrams/&quot; data-og-url=&quot;https://www.geeksforgeeks.org/unified-modeling-language-uml-sequence-diagrams/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Lvukw/hyVDxfMxTz/Jal8wzAuDcK2QQdeNSpsG0/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/c53uVQ/hyVABjO8Wu/JFzv6skmG0KW1b80sAYSlK/img.jpg?width=1000&amp;amp;height=500&amp;amp;face=0_0_1000_500,https://scrap.kakaocdn.net/dn/5CB8a/hyVDHiolTe/NnxKvgmUEUlHmtlKyZlSG0/img.jpg?width=1000&amp;amp;height=500&amp;amp;face=0_0_1000_500&quot;&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/unified-modeling-language-uml-sequence-diagrams/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.geeksforgeeks.org/unified-modeling-language-uml-sequence-diagrams/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Lvukw/hyVDxfMxTz/Jal8wzAuDcK2QQdeNSpsG0/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/c53uVQ/hyVABjO8Wu/JFzv6skmG0KW1b80sAYSlK/img.jpg?width=1000&amp;amp;height=500&amp;amp;face=0_0_1000_500,https://scrap.kakaocdn.net/dn/5CB8a/hyVDHiolTe/NnxKvgmUEUlHmtlKyZlSG0/img.jpg?width=1000&amp;amp;height=500&amp;amp;face=0_0_1000_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Sequence Diagrams | Unified Modeling Language (UML) - GeeksforGeeks&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.geeksforgeeks.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1710942762209&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;UML diagrams: What are they and how to use them | MiroBlog&quot; data-og-description=&quot;Find out everything you need to know about UML diagrams, including the different formats available and how to use them.&quot; data-og-host=&quot;miro.com&quot; data-og-source-url=&quot;https://miro.com/blog/uml-diagram/&quot; data-og-url=&quot;https://miro.com/blog/uml-diagram/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bbrYAx/hyVAJhQbzJ/BKr7NyFvj4rd40XT38dFf0/img.png?width=1774&amp;amp;height=942&amp;amp;face=0_0_1774_942,https://scrap.kakaocdn.net/dn/cxqMNV/hyVDtxG4Sn/cbLjgUqUt5iClfWuVHVfkK/img.png?width=1999&amp;amp;height=1122&amp;amp;face=0_0_1999_1122,https://scrap.kakaocdn.net/dn/bs6cr5/hyVDIhiWPp/6CGJkIRrkc77kjcKLTE0N0/img.png?width=1999&amp;amp;height=1122&amp;amp;face=0_0_1999_1122&quot;&gt;&lt;a href=&quot;https://miro.com/blog/uml-diagram/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://miro.com/blog/uml-diagram/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bbrYAx/hyVAJhQbzJ/BKr7NyFvj4rd40XT38dFf0/img.png?width=1774&amp;amp;height=942&amp;amp;face=0_0_1774_942,https://scrap.kakaocdn.net/dn/cxqMNV/hyVDtxG4Sn/cbLjgUqUt5iClfWuVHVfkK/img.png?width=1999&amp;amp;height=1122&amp;amp;face=0_0_1999_1122,https://scrap.kakaocdn.net/dn/bs6cr5/hyVDIhiWPp/6CGJkIRrkc77kjcKLTE0N0/img.png?width=1999&amp;amp;height=1122&amp;amp;face=0_0_1999_1122');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;UML diagrams: What are they and how to use them | MiroBlog&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Find out everything you need to know about UML diagrams, including the different formats available and how to use them.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;miro.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1710942762166&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;UML 배워보기 시리즈 #1 UML과 다이어그램의 종류&quot; data-og-description=&quot;컴퓨터 공학을 배웠다면 아마 대부분은 한 번쯤 UML 다이어그램을 듣거나 보셨을 겁니다.UML 다이어그램은 개발 과정에서 프로그램을 문서화 하는데 중요한 역할을 하지만, 자세히 배운 분들이 &quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@seolang2/UML-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-1-UML%EA%B3%BC-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8%EC%9D%98-%EC%A2%85%EB%A5%98&quot; data-og-url=&quot;https://velog.io/@seolang2/UML-배워보기-시리즈-1-UML과-다이어그램의-종류&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bBY8E9/hyVDGKziRy/DihuwTczcwG9uIuS2SYswK/img.png?width=609&amp;amp;height=422&amp;amp;face=0_0_609_422,https://scrap.kakaocdn.net/dn/MlnjA/hyVAAyr7Z4/XMV1nVSjlD9z622e8JD8KK/img.png?width=609&amp;amp;height=422&amp;amp;face=0_0_609_422,https://scrap.kakaocdn.net/dn/Cvvrb/hyVAMySMdZ/jI8acpveZkYtAVSoarpOvk/img.png?width=1418&amp;amp;height=753&amp;amp;face=0_0_1418_753&quot;&gt;&lt;a href=&quot;https://velog.io/@seolang2/UML-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-1-UML%EA%B3%BC-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8%EC%9D%98-%EC%A2%85%EB%A5%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@seolang2/UML-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-1-UML%EA%B3%BC-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8%EC%9D%98-%EC%A2%85%EB%A5%98&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bBY8E9/hyVDGKziRy/DihuwTczcwG9uIuS2SYswK/img.png?width=609&amp;amp;height=422&amp;amp;face=0_0_609_422,https://scrap.kakaocdn.net/dn/MlnjA/hyVAAyr7Z4/XMV1nVSjlD9z622e8JD8KK/img.png?width=609&amp;amp;height=422&amp;amp;face=0_0_609_422,https://scrap.kakaocdn.net/dn/Cvvrb/hyVAMySMdZ/jI8acpveZkYtAVSoarpOvk/img.png?width=1418&amp;amp;height=753&amp;amp;face=0_0_1418_753');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;UML 배워보기 시리즈 #1 UML과 다이어그램의 종류&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;컴퓨터 공학을 배웠다면 아마 대부분은 한 번쯤 UML 다이어그램을 듣거나 보셨을 겁니다.UML 다이어그램은 개발 과정에서 프로그램을 문서화 하는데 중요한 역할을 하지만, 자세히 배운 분들이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category> ️ 협업</category>
      <category>class diagram</category>
      <category>sequence diagram</category>
      <category>UML</category>
      <category>Usecase Diagram</category>
      <category>시퀀스 다이어그램</category>
      <category>유스케이스 다이어그램</category>
      <category>클래스 다이어그램</category>
      <author>iseunghan</author>
      <guid isPermaLink="true">https://iseunghan.tistory.com/477</guid>
      <comments>https://iseunghan.tistory.com/477#entry477comment</comments>
      <pubDate>Wed, 20 Mar 2024 22:49:30 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] N+1 문제와 해결 (feat. fetch join, EntityGraph)</title>
      <link>https://iseunghan.tistory.com/475</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  아래 실습에 진행한 모든 코드는 &lt;a href=&quot;https://github.com/iseunghan/iseunghan-Lab/issues/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;에 있습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JPA N+1이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 JPA를 사용하다보면, N+1 쿼리를 만나게 됩니다. 여기서 N+1이란 Team(1) &amp;harr; Member(N) 연관관계가 있다고 가정했을 때, 하나의 팀을 조회했지만 팀 내부에 있는 모든 멤버들이 함께 조회되면서 1+N 개의 쿼리가 발생하는 것을 의미합니다. 직접 테스트 코드를 통해 이러한 상황들을 해결할 수 있는 방법들을 살펴보고 각 상황이 또 어떤 사이드이펙트가 있는지도 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Entity 및 Repository 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습에 사용될 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;

    @Builder
    private Member(String name) {
        this.name = name;
    }

    public void updateTeam(Team team) {
        this.team = team;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Team {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = &quot;team&quot;, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();

    @Builder
    private Team(String name) {
        this.name = name;
    }

    public void addMember(Member member) {
        this.members.add(member);
        member.updateTeam(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface TeamRepository extends JpaRepository&amp;lt;Team, Long&amp;gt; {
    Optional&amp;lt;Team&amp;gt; findTeamByName(String name);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class Member_Lazy_Team_Lazy_Test {
    @Autowired private MemberRepository memberRepository;
    @Autowired private TeamRepository teamRepository;
    @PersistenceContext private EntityManager em;

    @BeforeEach
    void setup() {
        System.out.println(&quot;----------setup start-----------&quot;);
        Team team1 = Team.builder().name(&quot;team1&quot;).build();
        team1.addMember(Member.builder().name(&quot;member1-1&quot;).build());
        team1.addMember(Member.builder().name(&quot;member1-2&quot;).build());

        Team team2 = Team.builder().name(&quot;team2&quot;).build();
        team2.addMember(Member.builder().name(&quot;member2-1&quot;).build());
        team2.addMember(Member.builder().name(&quot;member2-2&quot;).build());

        Team team3 = Team.builder().name(&quot;team3&quot;).build();
        team3.addMember(Member.builder().name(&quot;member3-1&quot;).build());
        team3.addMember(Member.builder().name(&quot;member3-2&quot;).build());

        teamRepository.save(team1);
        teamRepository.save(team2);
        teamRepository.save(team3);

        clearPersistenceContext();
        System.out.println(&quot;----------setup end-----------&quot;);
    }

    @AfterEach
    void clear() {
        System.out.println(&quot;----------clear start-----------&quot;);
        memberRepository.deleteAll();
        teamRepository.deleteAll();
        clearPersistenceContext();
        System.out.println(&quot;----------clear end-----------&quot;);
    }

    private void clearPersistenceContext() {
        em.flush();
        em.clear();
    }

    @DisplayName(&quot;모든 팀을 조회하고, 지연로딩 된 멤버를 사용할 때 -&amp;gt; N+1이 발생한다.(team1,2,3 조회하는 쿼리 1개, team1,2,3에 대한 멤버 조회하는 쿼리 3개)&quot;)
    @Test
    void team_findAll_test() {
        clearPersistenceContext();

        System.out.println(&quot;----------team_findAll_test start-----------&quot;);
        List&amp;lt;Team&amp;gt; teamList = teamRepository.findAll();
        assertThat(teamList).hasSize(3);
        System.out.println(&quot;----------team_findAll_test mid-----------&quot;);
        teamList.stream()
                .map(Team::getMembers)
                .map(List::stream)
                .forEach(memberStream -&amp;gt; memberStream
                        .map(Member::getName)
                        .forEach(System.out::println)
                );
        System.out.println(&quot;----------team_findAll_test end-----------&quot;);
    }

    @DisplayName(&quot;모든 멤버를 조회하고, 지연로딩 된 팀을 사용할 때 -&amp;gt; N+1이 발생한다.(모든 멤버 조회 1개, 각 팀을 조회하는 쿼리 3개)&quot;)
    @Test
    void member_findAll_test() {
        clearPersistenceContext();

        System.out.println(&quot;----------member_findAll_test start-----------&quot;);
        List&amp;lt;Member&amp;gt; memberList = memberRepository.findAll();
        assertThat(memberList).hasSize(6);
        System.out.println(&quot;----------member_findAll_test mid-----------&quot;);
        memberList.stream()
                .map(Member::getTeam)
                .map(Team::getName)
                .forEach(System.out::println);
        System.out.println(&quot;----------member_findAll_test end-----------&quot;);
    }
}
&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;1. 모든 팀을 조회하고, 각 팀에 속한 멤버들의 이름을 출력했을 때, 쿼리가 어떻게 나가는 (team_findAll_test)팀 조회 후, 각 팀에 속한 멤버들을 사용할 때 추가 쿼리가 나가는 것을 알 수 있습니다. (N+1) 이전에는 각 멤버의 ID로 조회하는 쿼리가 나가서 4개의 쿼리가 아닌 &lt;a href=&quot;https://github.com/jojoldu/blog-code/blob/master/jpa-entity-graph/images/n1%EC%BF%BC%EB%A6%AC.png&quot;&gt;각 멤버를 조회하는 7개의 쿼리&lt;/a&gt;가 나갔었는데 &lt;a href=&quot;https://spring.io/blog/2023/08/31/this-is-the-beginning-of-the-end-of-the-n-1-problem-introducing-single-query&quot;&gt;언제부턴가 JPA 쪽에서 최적화&lt;/a&gt;를 했는지 team_id를 통해 조회하는 쿼리 덕분에 4개로 줄었습니다. (언제 최적화가 이뤄졌는지 아시는분 꼭 좀 알려주세요&amp;hellip;.)&lt;/p&gt;
&lt;pre id=&quot;code_1708998492574&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;----------team_findAll_test start-----------
Hibernate: 
    select
        t1_0.id,
        t1_0.name 
    from
        team t1_0
----------team_findAll_test mid-----------
Hibernate: 
    select
        m1_0.team_id,
        m1_0.id,
        m1_0.name 
    from
        member m1_0 
    where
        m1_0.team_id=?
member1-1
member1-2
Hibernate: 
    select
        m1_0.team_id,
        m1_0.id,
        m1_0.name 
    from
        member m1_0 
    where
        m1_0.team_id=?
member2-1
member2-2
Hibernate: 
    select
        m1_0.team_id,
        m1_0.id,
        m1_0.name 
    from
        member m1_0 
    where
        m1_0.team_id=?
member3-1
member3-2
----------team_findAll_test end-----------&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 모든 멤버를 조회하고, 각 멤버들의 팀의 이름을 출력했을 때, 쿼리가 어떻게 나가는지 (member_findAll_test)Member의 Team은 LAZY 전략으로 되어 있기 때문에 member를 조회하는 시점에는 member 조회 쿼리만 나가고, 실제 Team을 사용할 때 Team을 조회하는 쿼리가 나갑니다.&lt;/p&gt;
&lt;pre id=&quot;code_1708998525640&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;----------member_findAll_test start-----------
Hibernate: 
    select
        m1_0.id,
        m1_0.name,
        m1_0.team_id 
    from
        member m1_0
----------member_findAll_test mid-----------
Hibernate: 
    select
        t1_0.id,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.id=?
team1
team1
Hibernate: 
    select
        t1_0.id,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.id=?
team2
team2
Hibernate: 
    select
        t1_0.id,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.id=?
team3
team3
----------member_findAll_test end-----------&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯 실무에서 N+1 쿼리가 발생하면, 테스트 수준처럼 몇개가 아닌 몇천개에서 몇만개가 조회되는 문제가 발생하고 장애로 이어질 가능성이 큽니다. 어떻게 해결할 수 있는지 바로 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. FetchJoin 사용&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface TeamRepository extends JpaRepository&amp;lt;Team, Long&amp;gt; {
	@Query(value = &quot;select t from Team t join fetch t.members&quot;)
	List&amp;lt;Team&amp;gt; findTeamsFetchJoin();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@DisplayName(&quot;모든 팀을 조회하고, 지연로딩 된 멤버를 사용할 때 -&amp;gt; 단 1개의 쿼리만 나간다&quot;)
@Test
void team_findAll_test() {
    clearPersistenceContext();

    System.out.println(&quot;----------team_findAll_test start-----------&quot;);
    List&amp;lt;Team&amp;gt; teamList = teamRepository.findTeamsFetchJoin();
    assertThat(teamList).hasSize(3);
    System.out.println(&quot;----------team_findAll_test mid-----------&quot;);
    teamList.stream()
            .map(Team::getMembers)
            .map(List::stream)
            .forEach(memberStream -&amp;gt; memberStream
                    .map(Member::getName)
                    .forEach(System.out::println)
            );
    System.out.println(&quot;----------team_findAll_test end-----------&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀을 조회할 때, 팀에 속한 멤버들까지 한방에 Fetch Join 하는 방법입니다. 이렇게 되면 연관된 엔티티 또는 컬렉션을 프록시 객체가 아닌 즉시로딩으로 가져오기 때문에 쿼리는 하나만 발생하게 될 것 입니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;----------team_findAll_test start-----------
Hibernate: 
    select
        t1_0.id,
        m1_0.team_id,
        m1_0.id,
        m1_0.name,
        t1_0.name 
    from
        team t1_0 
    join
        member m1_0 
            on t1_0.id=m1_0.team_id
----------team_findAll_test mid-----------
member1-1
member1-2
member2-1
member2-2
member3-1
member3-2
----------team_findAll_test end-----------
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 Inner Join을 통해서 연관된 멤버들을 모두 조회해서 1차 캐시에 넣어두기 때문에, 멤버의 이름을 찍는 순간에도 추가 쿼리가 발생하지 않는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. EntityGraph 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fetch Join이 좋지만 문자열로 하드 코딩을 해야한다는 단점이 있습니다. 이럴 때는 EntityGraph를 사용할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface TeamRepository extends JpaRepository&amp;lt;Team, Long&amp;gt; {
	@EntityGraph(attributePaths = {&quot;members&quot;})
	@Query(value = &quot;select t from Team t&quot;)
	List&amp;lt;Team&amp;gt; findTeamsEntityGraph();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@DisplayName(&quot;모든 팀을 조회하고, 지연로딩 된 멤버를 사용할 때 -&amp;gt; 단 1개의 쿼리만 나간다&quot;)
@Test
void team_findAll_test() {
    clearPersistenceContext();

    System.out.println(&quot;----------team_findAll_test start-----------&quot;);
    List&amp;lt;Team&amp;gt; teamList = teamRepository.findTeamsEntityGraph();
    assertThat(teamList).hasSize(3);
    System.out.println(&quot;----------team_findAll_test mid-----------&quot;);
    teamList.stream()
            .map(Team::getMembers)
            .map(List::stream)
            .forEach(memberStream -&amp;gt; memberStream
                    .map(Member::getName)
                    .forEach(System.out::println)
            );
    System.out.println(&quot;----------team_findAll_test end-----------&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티 그래프의 장점은 기존 쿼리를 해치지 않고, 따로 @EntityGraph를 통해 특정 필드를 EAGER 로딩할 수 있다는 것입니다. 또한 attributePaths는 여러 개를 선언해서 가져올 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;----------team_findAll_test start-----------
Hibernate: 
    select
        t1_0.id,
        m1_0.team_id,
        m1_0.id,
        m1_0.name,
        t1_0.name 
    from
        team t1_0 
    left join
        member m1_0 
            on t1_0.id=m1_0.team_id
----------team_findAll_test mid-----------
member1-1
member1-2
member2-1
member2-2
member3-1
member3-2
----------team_findAll_test end-----------
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fetch Join과 동일하게 EAGER로 가져와 member를 사용할 때 추가쿼리가 발생하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 EntityGraph는 Fetch Join과 다른점이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;----------team_findAll_test start-----------
Hibernate: 
    select
        t1_0.id,
        m1_0.team_id,
        m1_0.id,
        m1_0.name,
        t1_0.name 
    from
        team t1_0 
    join
        member m1_0 
            on t1_0.id=m1_0.team_id
----------team_findAll_test mid-----------
member1-1
member1-2
member2-1
member2-2
member3-1
member3-2
----------team_findAll_test end-----------
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;----------team_findAll_test start-----------
Hibernate: 
    select
        t1_0.id,
        m1_0.team_id,
        m1_0.id,
        m1_0.name,
        t1_0.name 
    from
        team t1_0 
    left join
        member m1_0 
            on t1_0.id=m1_0.team_id
----------team_findAll_test mid-----------
member1-1
member1-2
member2-1
member2-2
member3-1
member3-2
----------team_findAll_test end-----------
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityGraph는 Join할 때 Outer Left Join을 한다는 점입니다. 이는 카테시안 곱(**Cartesian Product)**이 발생한다는 단점이 있기 때문에 되도록이면 Fetch Join을 사용하는 것을 추천드립니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1 해결을 위해 Fetch Join과 EntityGraph를 이용해 해결했는데요 과연 둘 중 뭘 써야 할까요? 저는 개인적으로 Fetch Join을 사용하는 것을 추천드립니다. 그 이유는 EntityGraph는 Outer Join을 사용하기 때문에 성능상 더 안좋기 때문입니다. 그렇다면 Fetch Join은 만능일까요? 다음 포스팅에는 &lt;a href=&quot;https://iseunghan.tistory.com/478&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Fetch Join이 과연 만능인가?&lt;/a&gt;에 대한 주제로 찾아뵙겠습니다. 부족한 글 읽어주셔서 감사합니다!&lt;/p&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://cheese10yun.github.io/jpa-nplus-1/&quot;&gt;JPA N+1 발생원인과 해결방법 - Yun Blog | 기술 블로그&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://s-y-130.tistory.com/184&quot;&gt;[JPA] N+1 문제 원인 및 해결방법&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85&quot;&gt;JPA 모든 N+1 발생 케이스과 해결책&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jojoldu.tistory.com/165&quot;&gt;JPA N+1 문제 및 해결방안&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://rocket-dev.co.kr/jpa-n1-%EB%AC%B8%EC%A0%9C-%EB%B0%8F-%ED%95%B4%EA%B2%B0%EB%B0%A9%EC%95%88/&quot;&gt;JPA | JPA N+1 문제 및 해결방안 - 개발 블로그&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gmoon92.github.io/spring/jpa/hibernate/n+1/2021/01/12/jpa-n-plus-one.html&quot;&gt;JPA 성능 N+1 문제와 해결 방법&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/questions/39516/fetch-%EC%A1%B0%EC%9D%B8-%EC%97%94%ED%8B%B0%ED%8B%B0-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4&quot;&gt;fetch 조인, 엔티티 그래프 질문입니다. - 인프런 | 질문 &amp;amp; 답변&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/questions/59632/fetch-join-%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8-%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4&quot;&gt;fetch join 관련 질문 드립니다!! - 인프런 | 질문 &amp;amp; 답변&lt;/a&gt;&lt;/p&gt;</description>
      <category>  JAVA/자바 ORM 표준 JPA 프로그래밍</category>
      <category>EntityGraph</category>
      <category>fetch join</category>
      <category>JPA</category>
      <category>JPA N+1</category>
      <category>N+1</category>
      <author>iseunghan</author>
      <guid isPermaLink="true">https://iseunghan.tistory.com/475</guid>
      <comments>https://iseunghan.tistory.com/475#entry475comment</comments>
      <pubDate>Tue, 27 Feb 2024 10:50:13 +0900</pubDate>
    </item>
    <item>
      <title>[Python] influxdb 삽입 시 timezone 주의사항 (feat. datetime.now()를 지양하자)</title>
      <link>https://iseunghan.tistory.com/474</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Intro&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬으로 InfluxDB에 데이터를 저장시키는 모듈을 개발하다 이상한점이 생겼습니다. KST로 넣었는데 이게 그대로 UTC로 들어가 있거나 분명 UTC로 저장시켰는데 KST로 저장되어있거나.. 희한한 현상이 발생했습니다. 그래서 파이썬 코드를 전부 까보며 influxDB 저장할 때 시간을 변환하는지와 서버 타임존과 관련이 있는지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버의 타임존이 다르면, influxdb에는 시간이 어떻게 찍힐까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TL;DR&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;  서버 시간에 따라서 `datetime.timestamp()` 함수가 UTC 시간으로 보정하여 변환한다. 그렇기 때문에 서버 타임존에 따라서 timestamp()의 결과가 달라질 뿐, influxdb_client 라이브러리를 통해 데이터 삽입 시에는 따로 시간이 변환되지 않는다. 입력 시간을 무조건 UTC로 간주한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공통 테스트 코드&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;client = InfluxDBClient(url='http://localhost:8086', token='my-secret-token', org=&quot;myorg&quot;)

now = datetime.now()

utc = pytz.timezone('UTC')
seoul = pytz.timezone('Asia/Seoul')

now_utc = datetime.utcnow()
now_kst = datetime.now(tz=seoul)
print(&quot;------------------------------------------&quot;)
print('UTC tz: ', now_utc)
print('KST tz: ', now_kst)
print(&quot;------------------------------------------&quot;)

utc_nano = int((now_utc.timestamp()) * 10 ** 9)
kst_nano = int((now_kst.timestamp()) * 10 ** 9)
print(&quot;UTC-NS: &quot;, utc_nano)
print(&quot;KST-NS: &quot;, kst_nano)
print(&quot;------------------------------------------&quot;)

utc_body = Point(f&quot;test-measure-{str(utc_nano)[-3:-1]}&quot;).time(utc_nano).field('utc', utc_nano)
kst_body = Point(f&quot;test-measure-{str(utc_nano)[-3:-1]}&quot;).time(kst_nano).field('kst', kst_nano)

write_api = client.write_api(write_options=SYNCHRONOUS)
write_api.write(bucket='test-bucket', org='myorg', record=kst_body)
write_api.write(bucket='test-bucket', org='myorg', record=utc_body)
write_api.close()
print(&quot;UTC-Influx-Point --&amp;gt; &quot;, utc_body)
print(&quot;KST-Influx-Point --&amp;gt; &quot;, kst_body)
print(&quot;------------------------------------------&quot;)

client.close()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case 1) 서버 시간: KST 설정&lt;/h3&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;/ # date
Fri Feb 23 12:22:05 KST 2024

/ # python3 Test.py
------------------------------------------
UTC tz:  2024-02-23 03:22:10.295505
KST tz:  2024-02-23 12:22:10.295507+09:00
------------------------------------------
UTC-NS:  1708626130295505152 # -9시간 됨, **2024-02-22T18:22:10.295Z**
KST-NS:  1708658530295506944 # -9시간 됨, **2024-02-23T03:22:10.295Z**
------------------------------------------
UTC-Influx-Point --&amp;gt;  test-measure-15 utc=1708626130295505152i 1708626130295505152
KST-Influx-Point --&amp;gt;  test-measure-15 kst=1708658530295506944i 1708658530295506944
------------------------------------------&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;219&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chzQ1a/btsFcRKigYM/RK46aABLukDIORJg3CnZm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chzQ1a/btsFcRKigYM/RK46aABLukDIORJg3CnZm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chzQ1a/btsFcRKigYM/RK46aABLukDIORJg3CnZm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchzQ1a%2FbtsFcRKigYM%2FRK46aABLukDIORJg3CnZm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;219&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;219&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;influxdb 결과&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;UTC, _time: &lt;code&gt;2024-02-22T18:22:10.295Z&lt;/code&gt;, _value: &lt;code&gt;1708626130295505200&lt;/code&gt; ,&lt;br /&gt;결론: UTC지만, 서버시간이 KST라 -9시간 보정됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHrW9i/btsFgQCTyby/VYKVDcsrU5EpqSUwOVV8ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHrW9i/btsFgQCTyby/VYKVDcsrU5EpqSUwOVV8ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHrW9i/btsFgQCTyby/VYKVDcsrU5EpqSUwOVV8ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHrW9i%2FbtsFgQCTyby%2FVYKVDcsrU5EpqSUwOVV8ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;501&quot; height=&quot;145&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;KST, _time: &lt;code&gt;2024-02-23T03:22:10.295Z&lt;/code&gt;, _value: &lt;code&gt;1708658530295507000&lt;/code&gt; ,&lt;br /&gt;결론: KST이고, 서버가 KST라 -9시간 보정됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVMUKE/btsFfvfbIfC/J9ZsJIvolCGaJak1ECl2f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVMUKE/btsFfvfbIfC/J9ZsJIvolCGaJak1ECl2f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVMUKE/btsFfvfbIfC/J9ZsJIvolCGaJak1ECl2f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVMUKE%2FbtsFfvfbIfC%2FJ9ZsJIvolCGaJak1ECl2f1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;496&quot; height=&quot;144&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case 2) 서버 시간: UTC 설정&lt;/h3&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;/ # date
Fri Feb 23 03:32:49 UTC 2024

/ # python3 Test.py
------------------------------------------
UTC-pytz:  UTC
Seoul-pytz:  Asia/Seoul
------------------------------------------
UTC tz:  2024-02-23 03:32:53.333882
KST tz:  2024-02-23 12:32:53.333884+09:00
------------------------------------------
UTC-NS:  1708659173333882112 # -0시간 됨, **2024-02-23T03:32:53.333Z**
KST-NS:  1708659173333883904 # -9시간 됨, **2024-02-23T03:32:53.333Z**
------------------------------------------
UTC-Influx-Point --&amp;gt;  test-measure-11 utc=1708659173333882112i 1708659173333882112
KST-Influx-Point --&amp;gt;  test-measure-11 kst=1708659173333883904i 1708659173333883904
------------------------------------------&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cns5Xs/btsFffi8CgU/yYO3le4dYEvdy9oO1DOkpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cns5Xs/btsFffi8CgU/yYO3le4dYEvdy9oO1DOkpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cns5Xs/btsFffi8CgU/yYO3le4dYEvdy9oO1DOkpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcns5Xs%2FbtsFffi8CgU%2FyYO3le4dYEvdy9oO1DOkpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;212&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;UTC, _time: &lt;code&gt;2024-02-23T03:32:53.333Z&lt;/code&gt;, _value: &lt;code&gt;1708659173333882000&lt;/code&gt; , 결론: UTC이므로 그대로 저장됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b58myW/btsFgStXfT0/XNcyAr1TpApwpiYKQfbYN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b58myW/btsFgStXfT0/XNcyAr1TpApwpiYKQfbYN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b58myW/btsFgStXfT0/XNcyAr1TpApwpiYKQfbYN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb58myW%2FbtsFgStXfT0%2FXNcyAr1TpApwpiYKQfbYN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;647&quot; height=&quot;176&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;KST, _time: &lt;code&gt;2024-02-23T03:32:53.333Z&lt;/code&gt;, _value: &lt;code&gt;1708659173333884000&lt;/code&gt; , 결론: KST이므로 -9시간 보정됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0yeu0/btsFfc7JXQO/ZWrKgOye5xTZDJKKAXY94k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0yeu0/btsFfc7JXQO/ZWrKgOye5xTZDJKKAXY94k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0yeu0/btsFfc7JXQO/ZWrKgOye5xTZDJKKAXY94k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0yeu0%2FbtsFfc7JXQO%2FZWrKgOye5xTZDJKKAXY94k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;196&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버 시간이 다르면 왜 시간이 다르게 저장될까? - 문제는 timestamp()이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 시간이 다를 때, 왜 시간이 다르게 보정됐을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 바로 &lt;code&gt;datetime.timestamp()&lt;/code&gt; 함수에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 datetime.now()를 사용하면, 기본적으로 tzinfo가 None으로 들어갑니다.&lt;/p&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;@classmethod
def now(cls, tz=None):
    &quot;Construct a datetime from time.time() and optional time zone info.&quot;
    t = _time.time()
    return cls.fromtimestamp(t, tz)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 timestamp 부분을 살펴보면, tzinfo가 None일 때 _mktime()을 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def timestamp(self):
      &quot;Return POSIX timestamp as float&quot;
      if self._tzinfo is None:
          s = self._mktime()
          return s + self.microsecond / 1e6
      else:
          return (self - _EPOCH).total_seconds()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_mktime 함수를 살펴보면, time.localtime(second) 함수를 호출하여, 시스템의 타임존에 따라 변환된 시간을 가지고 그 시간들을 계산해서 보정하는 것 같습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;def _mktime(self):
    &quot;&quot;&quot;Return integer POSIX timestamp.&quot;&quot;&quot;
    epoch = datetime(1970, 1, 1)
    max_fold_seconds = 24 * 3600
    t = (self - epoch) // timedelta(0, 1)
    def local(u):
        y, m, d, hh, mm, ss = _time.localtime(u)[:6]
        return (datetime(y, m, d, hh, mm, ss) - epoch) // timedelta(0, 1)

    # Our goal is to solve t = local(u) for u.
    a = local(t) - t
    u1 = t - a
    t1 = local(u1)
    if t1 == t:
        # We found one solution, but it may not be the one we need.
        # Look for an earlier solution (if `fold` is 0), or a
        # later one (if `fold` is 1).
        u2 = u1 + (-max_fold_seconds, max_fold_seconds)[self.fold]
        b = local(u2) - u2
        if a == b:
            return u1
    else:
        b = t1 - u1
        assert a != b
    u2 = t - b
    t2 = local(u2)
    if t2 == t:
        return u2
    if t1 == t:
        return u1
    # We have found both offsets a and b, but neither t - a nor t - b is
    # a solution.  This means t is in the gap.
    return (max, min)[self.fold](u1, u2)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론 - datetime.now()를 지양하고 datetime.now(tz)를 지향하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KST 타임존 설정된 서버에서 UTC 시간과 KST 둘 다 -9시간 보정된게 뭔가 이상했습니다. UTC는 그냥 냅둬야 되는게 아닌가? 라고 생각으로 다시 코드를 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 바로 &lt;code&gt;datetime.utcnow()&lt;/code&gt; 이 녀석입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;now_utc = datetime.utcnow()
# UTC tz:  2024-02-23 03:20:29.093107&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;utcnow 함수를 들어가봅시다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;@classmethod
def utcnow(cls):
    &quot;Construct a UTC datetime from time.time().&quot;
    t = _time.time()
    return cls.utcfromtimestamp(t)

@classmethod
def utcfromtimestamp(cls, t):
    &quot;&quot;&quot;Construct a naive UTC datetime from a POSIX timestamp.&quot;&quot;&quot;
    return cls._fromtimestamp(t, True, None)

@classmethod
def _fromtimestamp(cls, t, utc, tz):
    &quot;&quot;&quot;Construct a datetime from a POSIX timestamp (like time.time()).

    A timezone info object may be passed in as well.
    &quot;&quot;&quot;
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 바로 utc를 생성할 때, tz에 None을 주는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 utcnow()로 생성한 datetime의 tzinfo를 호출해보면 None으로 나옵니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; print(datetime.utcnow().tzinfo)
None&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 tzinfo가 None이므로 얘가 UTC인지 KST인지에 대한 정보가 없으므로 그냥 -9시간 보정이 들어간 것 입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;datetime.now()&amp;nbsp;의 사용을 지양하고,&amp;nbsp;datetime.now(tz=timezone.utc)&amp;nbsp;를 지향하자 입니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;dt.datetime.now(dt.timezone.utc)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;datetime.datetime(2024, 2, 23, 5, 34, 16, 146937, tzinfo=datetime.timezone.utc)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timezone 정보가 없다면 이 시간이 어떤 시간대인지 알기 어렵습니다. timezone을 설정해주고 다시 테스트를 해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;/ # date
Fri Feb 23 14:24:12 KST 2024

/ # python3 Test.py
------------------------------------------
UTC tz:  2024-02-23 05:24:15.156322+00:00
KST tz:  2024-02-23 14:24:15.156330+09:00
------------------------------------------
UTC-NS:  1708665855156322048
KST-NS:  1708665855156329984
------------------------------------------
UTC-Influx-Point --&amp;gt;  test-measure-04 utc=1708665855156322048i 1708665855156322048
KST-Influx-Point --&amp;gt;  test-measure-04 kst=1708665855156329984i 1708665855156329984
------------------------------------------&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보이시나요? 기존에 없었던 &lt;code&gt;UTC tz:  2024-02-23 05:24:15.156322+00:00&lt;/code&gt; 뒤에 &lt;code&gt;+00:00&lt;/code&gt; 이 붙었습니다. 바로 GMT+0이라는 정보가 추가된 것인데요. 이렇게 되면 KST 타임존을 가진 서버에서 timestamp() 함수를 호출했을 때 -9시간을 하지 않습니다. 그래서 결국 동일한 시간이 들어간 것을 알 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;번외 - 문자열로 저장한다면 꼭 timezone을 포함시키자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;influxdb에 문자열 형태로 넣고 싶으실 수 있습니다. UTC 시간에 Z를 붙여 UTC라는 표현을 해주고, KST로 변환한 시간을 넣어주는 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;client = InfluxDBClient(url='http://localhost:8086', token='my-secret-token', org=&quot;myorg&quot;)

now = datetime.now()

utc = pytz.timezone('UTC')
seoul = pytz.timezone('Asia/Seoul')

now_utc = datetime.now(tz=utc)
now_kst = datetime.now(tz=seoul)
print(&quot;------------------------------------------&quot;)
print('UTC tz: ', now_utc)
print('KST tz: ', now_kst)
print(&quot;------------------------------------------&quot;)

utc = now_utc.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
kst = now_kst.strftime('%Y-%m-%dT%H:%M:%S.%f')
print(f&quot;UTC-strftime: {utc}&quot;)
print(f&quot;KST-strftime: {kst}&quot;)
print(&quot;------------------------------------------&quot;)

utc_body = Point(f&quot;test-measure-1&quot;).time(utc).field('utc', 100)
kst_body = Point(f&quot;test-measure-1&quot;).time(kst).field('kst', 100)

write_api = client.write_api(write_options=SYNCHRONOUS)
write_api.write(bucket='test-bucket', org='in2wise', record=kst_body)
write_api.write(bucket='test-bucket', org='in2wise', record=utc_body)
write_api.close()

print(&quot;UTC-Influx-Point --&amp;gt; &quot;, utc_body)
print(&quot;KST-Influx-Point --&amp;gt; &quot;, kst_body)
print(&quot;------------------------------------------&quot;)

client.close()&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;/ # date
Fri Feb 23 15:02:50 KST 2024

/ # python3 Test.py
------------------------------------------
UTC tz:  2024-02-23 06:31:50.391616+00:00
KST tz:  2024-02-23 15:31:50.391623+09:00
------------------------------------------
UTC-strftime: 2024-02-23T06:31:50.391616Z
KST-strftime: 2024-02-23T15:31:50.391623
------------------------------------------
UTC-Influx-Point --&amp;gt;  test-measure-111 utc=100i 1708669910391616000 # GMT: 2024년 February 23일 Friday AM 6:31:50.391
KST-Influx-Point --&amp;gt;  test-measure-111 kst=100i 1708702310391623000 # GMT: 2024년 February 23일 Friday PM 3:31:50.391
------------------------------------------&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;utc 로우에는 실제 utc 시간을 넣었고, kst 로우에는 kst 시간을 넣었지만 client에서 따로 kst를 utc로 변환하거나 하는 작업은 없었습니다. 여기서 &lt;a href=&quot;https://www.timeanddate.com/worldclock/timezone/zulu&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Z&lt;/a&gt;는 의미가 없어보입니다. 타임존에 대한 정보가 없으니 라이브러리는 time에 들어가는 시간을 utc로 간주하고 그대로 저장시킵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타임존을 꼭 포함시키자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/datetime.html#strftime-strptime-behavior&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;를 참고해서 타임존을 포함시키도록 합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;utc = now_utc.strftime('%Y-%m-%dT%H:%M:%S.%f %Z') # 또는 %z를 사용
kst = now_kst.strftime('%Y-%m-%dT%H:%M:%S.%f %Z')&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/ # date
Fri Feb 23 15:02:50 KST 2024

/ # python3 Test.py
------------------------------------------
UTC tz:  2024-02-23 06:34:43.474180+00:00
KST tz:  2024-02-23 15:34:43.474191+09:00
------------------------------------------
UTC-strftime: 2024-02-23T06:34:43.474180 UTC
KST-strftime: 2024-02-23T15:34:43.474191 KST
------------------------------------------
UTC-Influx-Point --&amp;gt;  test-measure-111 utc=100i 1708670083474180000 # GMT: 2024년 February 23일 Friday AM 06:34:43.474
KST-Influx-Point --&amp;gt;  test-measure-111 kst=100i 1708670083474191000 # GMT: 2024년 February 23일 Friday AM 06:34:43.474
------------------------------------------&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 뒤에 타임존에 대한 정보가 있으니, influxdb-client 라이브러리가 UTC로 변환하여 저장시키는 것을 확인할 수 있습니다. 이 테스트로 저희는 다음과 같은 결론을 내릴 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;influxdb Client는 time에 넣은 시간이 따로 타임존이 없다면&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: center;&quot;&gt;(Z의 경우 포함)&lt;/span&gt; 무조건 UTC로 본다. 만약 타임존이 포함된 문자열이라면, 라이브러리는 UTC로 변환하여 저장시킨다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;figure id=&quot;og_1708671152178&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;datetime &amp;mdash; Basic date and time types&quot; data-og-description=&quot;Source code: Lib/datetime.py The datetime module supplies classes for manipulating dates and times. While date and time arithmetic is supported, the focus of the implementation is on efficient attr...&quot; data-og-host=&quot;docs.python.org&quot; data-og-source-url=&quot;https://docs.python.org/ko/3/library/datetime.html#strftime-strptime-behavior&quot; data-og-url=&quot;https://docs.python.org/3/library/datetime.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/x5d4z/hyVmZd4c0T/7F6oCJWrVMKNl4KZZn2ZB0/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200&quot;&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/datetime.html#strftime-strptime-behavior&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.python.org/ko/3/library/datetime.html#strftime-strptime-behavior&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/x5d4z/hyVmZd4c0T/7F6oCJWrVMKNl4KZZn2ZB0/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;datetime &amp;mdash; Basic date and time types&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Source code: Lib/datetime.py The datetime module supplies classes for manipulating dates and times. While date and time arithmetic is supported, the focus of the implementation is on efficient attr...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.python.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1708671112875&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;datetime &amp;mdash; Basic date and time types&quot; data-og-description=&quot;Source code: Lib/datetime.py The datetime module supplies classes for manipulating dates and times. While date and time arithmetic is supported, the focus of the implementation is on efficient attr...&quot; data-og-host=&quot;docs.python.org&quot; data-og-source-url=&quot;https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow&quot; data-og-url=&quot;https://docs.python.org/3/library/datetime.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bVvzQj/hyVqnRTC5b/3btymLRNZfzYp5eAUxUGJ1/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200&quot;&gt;&lt;a href=&quot;https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bVvzQj/hyVqnRTC5b/3btymLRNZfzYp5eAUxUGJ1/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;datetime &amp;mdash; Basic date and time types&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Source code: Lib/datetime.py The datetime module supplies classes for manipulating dates and times. While date and time arithmetic is supported, the focus of the implementation is on efficient attr...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.python.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1708671112474&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Epoch Converter&quot; data-og-description=&quot;Convert Unix Timestamps (and many other date formats) to regular dates.&quot; data-og-host=&quot;www.epochconverter.com&quot; data-og-source-url=&quot;https://www.epochconverter.com/&quot; data-og-url=&quot;https://www.epochconverter.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sGBw6/hyVqopJLBv/sSqNLiyjiitfG58kYsV0ck/img.png?width=1186&amp;amp;height=342&amp;amp;face=0_0_1186_342&quot;&gt;&lt;a href=&quot;https://www.epochconverter.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.epochconverter.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sGBw6/hyVqopJLBv/sSqNLiyjiitfG58kYsV0ck/img.png?width=1186&amp;amp;height=342&amp;amp;face=0_0_1186_342');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Epoch Converter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Convert Unix Timestamps (and many other date formats) to regular dates.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.epochconverter.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>  Python</category>
      <category>datetime</category>
      <category>influxdb 시간 불일치</category>
      <category>influxdb-client timezone not converted</category>
      <category>timestamp</category>
      <category>tzinfo</category>
      <category>국제화 데이터</category>
      <category>타임존</category>
      <author>iseunghan</author>
      <guid isPermaLink="true">https://iseunghan.tistory.com/474</guid>
      <comments>https://iseunghan.tistory.com/474#entry474comment</comments>
      <pubDate>Fri, 23 Feb 2024 15:37:13 +0900</pubDate>
    </item>
  </channel>
</rss>