<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Developer Sean</title>
    <link>https://seandailytech.tistory.com/</link>
    <description>좋은 개발자가 되기 위한 기록</description>
    <language>ko</language>
    <pubDate>Tue, 12 May 2026 16:35:11 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>devsean</managingEditor>
    <item>
      <title>11. 대규모, 대고객, 커머스 도메인을 경험했던 네 번째 프로젝트</title>
      <link>https://seandailytech.tistory.com/47</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #434343;&quot;&gt;들어가며&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일본 여행을 다녀와서 다음 날 바로 프로젝트에 투입이 되었다. 결혼을 앞두고 친한 친구와 여행을 다녀왔는데 그 주와, 그 다음 주를 고민하다가 그 주로 선택해서 정말 다행이었다. 들어간 날부터 계속해서 격무를 이어갔기 때문이었다. 내가 담당했던 업무는 Google Analytics 클릭 이벤트 태깅이었는데, 3개월 동안 총 346개 영역에 태깅을 붙였다. Typescript를 활용했으며 React의 Hook, Context, useEffect와 같은 기초적인 개념들에 대해서도 익힐 수 있었다. &lt;b&gt;대규모/대고객 &lt;b&gt;커머스 &lt;/b&gt;프로젝트&lt;/b&gt;를 했다는 점이 개발자로서 매력적이었다. 이번에는 특히 &lt;b&gt;소프트 스킬(협업)&lt;/b&gt;에 대해 느낀 점이 많았고, 솔루션이 아닌 소프트웨어 구축 프로젝트를 수행하며 이 프로젝트의 &lt;b&gt;도메인&lt;/b&gt;에 대해서도 생각을 많이 해볼 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #434343;&quot;&gt;협업&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;협업 측면에서 먼저 정리해보면, 먼저 &lt;b&gt;배포 체계와&lt;/b&gt; &lt;b&gt;형상관리(Git)&lt;/b&gt;에 대해 고민해 볼 수 있어 좋았다. 대규모 프로젝트이다보니 dev/stg를 나누어서 관리했던 부분이라던지, PR을 승인받아 머지를 했는데 내 작업을 어떻게 하면 보다 명확하게 드러낼 수 있을지 커밋 단위로 잘 나누는 방법을 고민했다. 자연스럽게 다른 개발자들이 작성해 둔 커밋을 참고하면서 개발을 해나갔다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소프트웨어 프로젝트에서 모든 커밋은 의도를 담고 있다. 만약 그렇지 않다면 해당 커밋은 올바르게 된 것이 아니라고 생각한다. 그러므로 커밋은 각 작업 단위로 올바르게 작성되어야 하며, 메시지 또한 이러한 단위를 의미있고 적절하게 표현할 수 있어야 한다. &lt;b&gt;&amp;lsquo;개발자는 코드로 말한다.&amp;rsquo;는 이야기를 종종 들어왔는데, 이런 뜻이기도 하다는 것을 알게 되었다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;협력사 분들과 직접 소통하며 일할 수 있어 좋은 경험이었다. &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비록 다른 회사 소속이었음에도 명확한 업무 요청에는 제대로 응해주셨고, 어떨 때는 내 코드를 고쳐주기도 하셨다. 프로젝트 셋팅에 이슈가 있거나, 구현 상 고민이 될때는 같이 고민해주기도 하셨다. 함께 일하는 분들을 위해 코드를 좀 더 가독성 있게, 그리고 의도를 드러내기 위해 최선을 다했다. 원활한 협업을 위해서는 자신의 업무를 잘 파악하고, 요구사항을 명확히 해서 신뢰를 얻어야 함을 상기할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;협업을 하며 가장 크게 느꼈던 점은 &amp;lsquo;사람의 중요성&amp;rsquo;이었다.&lt;/b&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;lt;맨먼스 미신&amp;gt;이라는 책이 있다. 프로젝트의 성패는 투입 인력 숫자에 달려있다는 미신이 있지만 정말 미신에 불과함을 이번 프로젝트를 하면서 확실히 느꼈다. 대규모 프로젝트에 걸맞게 200명 이상의 인원들이 투입되어 있었지만 늘 상황이 좋지만은 않았다. 오히려 사람들이 많아지니 프로젝트의 복잡도가 훨씬 높아지는 것 같았다. 차라리 소수의 고정 인원으로, 처음부터 끝까지 수행했다면 더 나은 결과물을 낼수도 있지 않았을까? 하는 생각이 들기도 했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #434343;&quot;&gt;도메인&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개발자 친구가 &amp;lsquo;개발자로써 커머스 도메인을 경험해보는 게 의미가 크다.&amp;rsquo;고 얘기를 해줬는데, 왜 그런 이야기를 했는지 이해할 수 있었다. 커머스 도메인이란 일상 생활에서 물건을 사고 파는 플랫폼을 온라인 세계로 옮겨놓는 분야이다. 사람들이 더 쉽고 빠르게 소비할 수 있는 공간을 만드는 것이며, 그러므로 상품이 보여지는 화면과 주문/결제/정산 등의 실제 거래가 발생하는 서버 영역 모두가 중요하다. 내가 담당했던 유저 행동 트래킹과 메일/알림 등을 전송하는 배치 등 다양한 기술을 적용해 볼 여지도 크다. 쇼핑은 사람이라면 누구나 하는 행위이고, 또 주요한 여가 생활 중에 하나이기도 하다. &lt;b&gt;유저가 오래, 또 빈번히 머물수록 이를 편리하게 해줄 기술도 필요하기 마련이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;그래서 커머스 도메인이 재미있었다.&lt;/b&gt; 근무했던 건물 바로 앞에 백화점이 있었는데 산책을 하다보면 내가 화면에서 보던 페이지와 브랜드들이 보였다. 실세계의 무언가를 그대로 옮겨두었다는 점이 흥미로웠다. 그리고 대고객이었기 때문에 내가 기여한 것들이 유저와 밀접한 관련이 있어서, 임팩트를 바로 볼 수 있다는 점도 재밌었다. 개발자 아닌 친구들에게 내가 만든 프로젝트를 보여주고 또 쉽게 이해시킬 수 있어 뿌듯함도 컸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #434343;&quot;&gt;사람들의 삶을 편리하게 만드는 개발자&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;많은 사람들의 생활에 직접 영향을 줄 수 있는 프로젝트를 했다는 점에서, 의미있었고 &lt;/span&gt;또 재밌었던 프로젝트였다. 내가 왜 개발자가 되고 싶어했는지 다시금 느낄 수 있었다. Google Analytics, Typescript, Git 등 기술적으로 접했던 내용들을 더 천천히 공부해보고 싶다. 소프트웨어 프로젝트를 할 때 '언어나 기술은 도구'라는 생각이 더욱 확고해지기도 했다. &lt;b&gt;중요한 것은 사람들의 삶에 가치를 가져다 주는 것이기 때문이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>회고</category>
      <category>개발자</category>
      <category>대고객</category>
      <category>대규모</category>
      <category>맨먼스 미신</category>
      <category>커머스</category>
      <category>협업</category>
      <category>회고</category>
      <author>devsean</author>
      <guid isPermaLink="true">https://seandailytech.tistory.com/47</guid>
      <comments>https://seandailytech.tistory.com/47#entry47comment</comments>
      <pubDate>Mon, 20 Apr 2026 19:28:34 +0900</pubDate>
    </item>
    <item>
      <title>Python을 활용한 비동기 처리(Asyncio, Coroutine)</title>
      <link>https://seandailytech.tistory.com/46</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 프로젝트에서 FastAPI를 활용한 백엔드 개발을 수행했다. FastAPI 프레임워크는 ASGI 규약을 따르는 웹 어플리케이션으로, 비동기 처리를 지원하여 높은 성능을 뽑아낼 수 있다. 비동기 관련 개념에 대해 정리하고, Python에서는 어떻게 비동기 처리를 수행하는지 예제 코드와 함께 살펴보고자 한다.&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;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;b&gt;호출한 쪽의 제어 흐름을 Block하면 블로킹, 그렇지 않으면 논블로킹&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;해당 작업이 수행되어 마무리 될 때까지 &lt;b&gt;기다리는 것이 동기&lt;/b&gt;, &lt;b&gt;기다리지 않는 것이 비동기&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동기/비동기&lt;/b&gt;는 &lt;b&gt;작업을 호출하는&lt;/b&gt; &lt;b&gt;입장&lt;/b&gt;에서, &lt;b&gt;블로킹/논블로킹&lt;/b&gt;은 &lt;b&gt;작업을 받아 수행하는 입장&lt;/b&gt;에서의 개념&lt;/li&gt;
&lt;li&gt;일반적으로는 &lt;b&gt;동기 - Blocking&lt;/b&gt;, &lt;b&gt;비동기 - Non-Blocking&lt;/b&gt; 짝으로 연결됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Asyncio와 간단한 사용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python 3.4부터 &lt;b&gt;asyncio&lt;/b&gt;를 공식적인 비동기 라이브러리로 제공한다. asyncio는 이벤트 루프 기반으로 코루틴이라는 비동기 함수들의 실행을 관리한다. 비동기 처리를 통해 여러 작업을 &lt;b&gt;동시에(concurrent, not parallel) 수행&lt;/b&gt;할 수 있게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;동시성과 병렬성&lt;br /&gt;&lt;/b&gt;동시성은 하나의 주체가 여러 작업을 전환하면서 수행되는 과정이고, 병렬성은 말 그대로 여러 주체가 여러 작업을 수행되는 과정이다. 하나의 이벤트 루프는 하나의 스레드 안에서 동시적으로(concurrent) 작업을 수행한다.&lt;b&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 사용 예시를 확인해보자. say_after 함수는 delay 만큼 대기 후 입력한 문자열을 출력해준다. 동기식으로 실행한다면 delay로 넘겨준 1초 + 2초 = 3초가 소요되지만, 비동기식으로 실행하면 두 작업이 동시에 수행되어 2초가 소요된다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def say_after(delay, what):
    time.sleep(delay)
    print(what)

def main():
    start = time.time()
    say_after(1, 'hello')
    say_after(2, 'world')
    end = time.time()
    print(f&quot;Execution time : {end-start}&quot;)
main() # Execution time : 3.031189441680908
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    start = time.time()
    # create_task 하는 순간 작업 등록 후 이벤트 루프에서 실행
    task1 = asyncio.create_task(say_after(1, 'hello'))
    task2 = asyncio.create_task(say_after(2, 'world'))
    # await으로 제어권을 넘긴다. 
    # 이벤트 루프는 다른 await이 걸린 라인도 동시에 실행하므로, 작업 등록 순서는 상관 없다.
    # hello가 1초 뒤에 출력되고, world가 2초 뒤에 출력
    await task2
    await task1
    end = time.time()
    print(f&quot;Execution time : {end-start}&quot;)

asyncio.run(main()) # Execution time : 2.0171315670013428
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코루틴과 async/await&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 비동기 작업을 수행할 수 있는 함수를 의미한다. 함수 앞에 async 키워드를 붙여 호출 시 코루틴 객체를 반환할 수 있도록 하고, 이러한 코루틴 객체는 해당 코루틴 객체를 호출한 코루틴 내에서 await를 만나면 작업이 완료될 때까지 기다린다. 좀 더 명확하게 이야기하면, &lt;b&gt;await을 하게 되면 실행의 제어권이 해당 코루틴으로 넘어가게 된다&lt;/b&gt;는 것이다. 최초의 진입점이 되는 코루틴은 asyncio.run() 함수를 통해 실행이 되고, 코루틴 내부에서 다른 코루틴이 연쇄적으로 호출된다. 이러한 과정을 코루틴 체인이라고 한다.&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;p data-ke-size=&quot;size16&quot;&gt;그렇다면 코루틴들은 어떻게 관리되어 실행될까? 먼저, 코루틴은 그 자체로는 코루틴 객체를 반환할 뿐, 스스로 실행되지 않는다. asyncio가 제공하는 이벤트 루프에 Task로 등록됨으로써 작업을 수행하게 된다. 최초 코루틴 실행 시 asyncio.run()을 활용하는데, asyncio.run이 호출되며 현재 스레드에 새로운 이벤트가 생성된다. 이러한 이벤트 루프는 인자로 넘어오는 코루틴을 Task로 예약하여 실행하고(이 과정에서 당연히 코루틴 체인 내의 Task들도 연쇄적으로 등록될 것이다) 코루틴이 포함하는 모든 Task가 완료되면 이벤트 루프가 닫히게 된다. (asyncio.run()이 이벤트 루프를 생성하고 종료하게 됨)&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;Task 객체와 Future 객체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task 객체는 생성되면서 이벤트 루프에 해당 코루틴이 실행되도록 예약을 한다. Task 객체와 Future 객체에 대해 이해할 필요가 있다. 먼저 Future 객체는 어떤 작업의 상태(PENDING, CANCELLED, DONE)를 가진다. 또한 작업의 상태가 DONE일 때 호출될 함수를 등록할 수도 있는데, 이를 통해 이벤트 루프가 &amp;lsquo;루프를 돌면서&amp;rsquo; 상태를 체크하고 완료되었을 때 그 작업을 실행할 수 있도록 한다. Task 객체는 이러한 Future 객체를 상속하고, 생성 시 이벤트 루프에게 자신이 가지고 있는 코루틴이 실행되도록 요청한다. Task 객체는 Javascript의 Promise와 유사한 개념으로 볼 수도 있다.&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;Task를 등록한 이벤트 루프의 실행 과정&lt;/h3&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;진입점 코루틴에서, Task 실행 예약을 건다(create_task)&lt;/li&gt;
&lt;li&gt;각 task가 await를 만나면, 함수에서 주도권을 코루틴 체인(await 연쇄 체인)을 따라 내려준다.&lt;/li&gt;
&lt;li&gt;그 코루틴 체인에서 sleep 혹은 io 관련 코루틴을 만나게 된다.&lt;/li&gt;
&lt;li&gt;끝에 만난 코루틴은 퓨쳐 객체를 반환하고, 거기에는 PENDING, CANCELLED, DONE과 같은 상태가 저장되어 있다. 뭐가 됐든 퓨쳐 객체는 자기 자신이 가진 코루틴 실행을 이벤트 루프에 콜백으로 등록해두고, DONE이 될 때까지 기다리게 한 다음 이벤트 루프에 제어권을 넘긴다.&lt;/li&gt;
&lt;li&gt;그러면 이벤트 루프는 await가 걸린 다음 Task를 우선 실행하고 4번과 동일한 작업을 수행한다.&lt;/li&gt;
&lt;li&gt;DONE이 된 것부터 순차적으로 수행하고, 모든 await이 끝날때까지 기다린다. 이벤트 루프는 모든 task를 concurrent하게 수행하므로, await의 순서는 상관없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;I/O 성능 향상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 비동기적으로 처리하게 되면, 메인 작업의 실행을 블로킹하지 않게 되어 성능이 비약적으로 향상되게 된다. 외부 연동, DB I/O, 파일 I/O 와 같은 작업이 많다면 FastAPI의 비동기를 활용하는 게 좋다.&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;p data-ke-size=&quot;size16&quot;&gt;처음 시작하는 파이썬 2판, 15장 프로세스와 동시성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leffept.tistory.com/512&quot;&gt;[Python]파이썬 비동기 프로그래밍 동작 원리에 대해서 (feat. 이벤트 루프)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767698625103&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;[Python]파이썬 비동기 프로그래밍 동작 원리에 대해서 (feat. 이벤트 루프)&quot; data-og-description=&quot;안녕하세요, 오늘은 파이썬 비동기 프로그래밍 동작 원리에 대해서 알아보려고 합니다. 파이썬의 비동기는 이벤트 루프를 통해 동작하고 있다는 정도의 이해만 한 채로 개발을 하다 보니 문득 &quot; data-og-host=&quot;leffept.tistory.com&quot; data-og-source-url=&quot;https://leffept.tistory.com/512&quot; data-og-url=&quot;https://leffept.tistory.com/512&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bgEtZ6/hyZRhfcRxZ/U1xvfLW9R97050Tl6zNrp0/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/Txaui/hyZPG2nURq/RHf5GMsUu7EQ1LVH8cIczk/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/zDNf2/hyZRgUT2Tl/s5NQGTWT5rHO7PxNOVDdnK/img.png?width=1280&amp;amp;height=730&amp;amp;face=0_0_1280_730&quot;&gt;&lt;a href=&quot;https://leffept.tistory.com/512&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leffept.tistory.com/512&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bgEtZ6/hyZRhfcRxZ/U1xvfLW9R97050Tl6zNrp0/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/Txaui/hyZPG2nURq/RHf5GMsUu7EQ1LVH8cIczk/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/zDNf2/hyZRgUT2Tl/s5NQGTWT5rHO7PxNOVDdnK/img.png?width=1280&amp;amp;height=730&amp;amp;face=0_0_1280_730');&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;[Python]파이썬 비동기 프로그래밍 동작 원리에 대해서 (feat. 이벤트 루프)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요, 오늘은 파이썬 비동기 프로그래밍 동작 원리에 대해서 알아보려고 합니다. 파이썬의 비동기는 이벤트 루프를 통해 동작하고 있다는 정도의 이해만 한 채로 개발을 하다 보니 문득&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leffept.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://it-eldorado.com/posts/5ba540c3-98fa-4559-a673-cf232f6978ad&quot;&gt;Python 비동기 프로그래밍 동작 원리 (asyncio) :: IT 엘도라도&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767698630018&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;Python 비동기 프로그래밍 동작 원리 (asyncio) :: IT 엘도라도&quot; data-og-description=&quot;JavaScript와 달리 Python은 비동기 프로그래밍에 어색하다. 애초에 JavaScript는 비동기 방식으로 동작하도록 설계된 언어인 반면, Python은 동기 방식으로 동작하도록 설계된 언어이기 때문이다. 그래&quot; data-og-host=&quot;it-eldorado.com&quot; data-og-source-url=&quot;https://it-eldorado.com/posts/5ba540c3-98fa-4559-a673-cf232f6978ad&quot; data-og-url=&quot;https://it-eldorado.com/posts/5ba540c3-98fa-4559-a673-cf232f6978ad&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/danASc/hyZRorRMN2/zgKaCOdGUeZKVNb5m4lZaK/img.png?width=2052&amp;amp;height=1080&amp;amp;face=0_0_2052_1080,https://scrap.kakaocdn.net/dn/rWves/hyZRfVZBCG/M9EFkHt65rE0boi2nn0F31/img.png?width=2052&amp;amp;height=1080&amp;amp;face=0_0_2052_1080,https://scrap.kakaocdn.net/dn/E7AyS/hyZRpK4TGQ/6qIHKVMlSI4hJOKWhAL32k/img.png?width=1280&amp;amp;height=730&amp;amp;face=0_0_1280_730&quot;&gt;&lt;a href=&quot;https://it-eldorado.com/posts/5ba540c3-98fa-4559-a673-cf232f6978ad&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://it-eldorado.com/posts/5ba540c3-98fa-4559-a673-cf232f6978ad&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/danASc/hyZRorRMN2/zgKaCOdGUeZKVNb5m4lZaK/img.png?width=2052&amp;amp;height=1080&amp;amp;face=0_0_2052_1080,https://scrap.kakaocdn.net/dn/rWves/hyZRfVZBCG/M9EFkHt65rE0boi2nn0F31/img.png?width=2052&amp;amp;height=1080&amp;amp;face=0_0_2052_1080,https://scrap.kakaocdn.net/dn/E7AyS/hyZRpK4TGQ/6qIHKVMlSI4hJOKWhAL32k/img.png?width=1280&amp;amp;height=730&amp;amp;face=0_0_1280_730');&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;Python 비동기 프로그래밍 동작 원리 (asyncio) :: IT 엘도라도&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JavaScript와 달리 Python은 비동기 프로그래밍에 어색하다. 애초에 JavaScript는 비동기 방식으로 동작하도록 설계된 언어인 반면, Python은 동기 방식으로 동작하도록 설계된 언어이기 때문이다. 그래&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;it-eldorado.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Programming Language/Python</category>
      <category>asyncio</category>
      <category>Concurrent</category>
      <category>Coroutine</category>
      <category>fastapi</category>
      <category>Python</category>
      <category>비동기</category>
      <author>devsean</author>
      <guid isPermaLink="true">https://seandailytech.tistory.com/46</guid>
      <comments>https://seandailytech.tistory.com/46#entry46comment</comments>
      <pubDate>Tue, 6 Jan 2026 20:26:53 +0900</pubDate>
    </item>
    <item>
      <title>10. 2025년 개발자 연말 회고</title>
      <link>https://seandailytech.tistory.com/45</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발자로서 자리를 잡았던 한 해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 하반기부터 일을 시작해 어느덧 1년 반이 흘렀다. 올 한 해 업무적으로 많은 일들이 있었고, 하나씩 해결하면서 성장할 수 있었다. 소프트웨어 개발자로서의 업무와 생활에 익숙해졌고, 자리를 잡았던 한 해였다. 2025년에는 어떤 일을 했는지 정리해보고, 잘했던 점 3가지와 아쉬웠던 점 3가지를 적어보았다.&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;2025년에 내가 했던 일들&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;프로젝트 파견 근무
&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://seandailytech.tistory.com/26&quot;&gt;솔루션 커스터마이징 개발&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://seandailytech.tistory.com/32&quot;&gt;신규 서비스 개발&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://seandailytech.tistory.com/43&quot;&gt;상주 기술 지원&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;솔루션 유지보수
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용 문의 대응&lt;/li&gt;
&lt;li&gt;추가 개발&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사내 AI 교육 이수
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM&lt;/li&gt;
&lt;li&gt;MS Azure&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;블로그 다시 시작&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://seandailytech.tistory.com/44&quot;&gt;추석 연휴 스터디&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2025년에 잘했던 점 3가지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;합리적으로 생각하고 적극적으로 의견을 제시했던 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 개발자로서 가장 잘한 점은 프로젝트를 잘 해내기 위해 최선을 다했던 것이다. 3개의 프로젝트를 했는데 모두 쉽지 않은 상황에 놓여있었다. 매일 밤 10시를 넘겨 퇴근하면서 API 개발을 전부 다 도맡아 하기도 했다. '신입 사원&amp;rsquo;이라는 명목으로 적당히 하다가 나올 수도 있었지만, 그러기는 싫었다. 소프트웨어 개발자는 문제를 해결하고 고객에게 가치를 제공하는 사람이다. 이러한 직업 의식과 보람에 이끌려 시작한 일인데 쉽게 내려놓고 싶지 않았다.&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;그래서 적극적으로 문제 해결에 임했다. 진행이 어려운 부분은 PM에게 얘기해서 개선하고, 개발 작업에 필요한 지원이 있으면 고객사 담당자나 협력사 개발자 분들께 요청드렸다. 비록 회사에서의 직급은 제일 낮을 지라도, 내가 맡은 개발 업무나 프로젝트 전반적으로 봤을 때 내 의견이 맞으면 쉽게 물러서지 않았다. 물론 내가 놓친 부분이 있다면 바로 수긍했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사하게도 함께 프로젝트를 했던 분들께서도 나를 잘 도와주셨다. 타당한 의견이라면 잘 수용해주셨고, 필요한 지원이 있다면 해주셨다. 회사로부터 작게나마 프로젝트 인센티브 명목의 수당이나 격려품을 받기도 했고, 고객으로부터 감사 메일을 받기도 했다. 힘든 와중에도 고객이 전문가로 대우해주실 때는 뿌듯했다. 그러나 가장 큰 수확은 내 스스로 어떤 개발자로 성장해야 할 지에 대한 확신을 갖게 된 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;추석 연휴 스터디 챌린지에 참여했던 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 추석에는 생전 처음으로 모르는 사람들과 함께 스터디를 했다. 인프런에서 각자 듣고 싶었던 강의를 하나 선정해서 CTO인 향로님과 함께 완강하는 챌린지였다. 매일 하나 이상의 강의를 듣고 인증하는 미션이 있었다. 챌린지에 참여하면 강의를 30% 할인해주는 이벤트가 있었는데, 연휴도 알차게 보낼 겸해서 참여했다. 막상 챌린지를 마치고 나니, 강의를 저렴하게 들었던 것 이상으로 좋은 점이 있었다. '누군가와 함께 공부한다는 사실'은 그 자체만으로도 내게 큰 힘이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 항상 &amp;lsquo;공부는 혼자서 하는 것&amp;rsquo;이라는 생각을 가지고 있었다. 학창 시절부터 독학 및 인강이 체질에 잘 맞았는데, 공부 방향을 잡고 내용을 선정하는 데 있어 자유도가 높았기 때문이었다. 효율적이지만, 꾸준히 하기 위해서는 많은 의지력이 필요했다. 특히나 업무를 하면서 공부를 하려니 더욱 그랬고, 자꾸 중간에 그만두게 되었다(지난 한 해동안 이 블로그에 글이 올라왔던 주기만 봐도 그렇다).&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;이번에는 달랐다. 사람들과 함께 같은 미션(하루 한 강 이상 듣기)을 수행하면서, 나와 타협하려 하다가도 약속을 지키기 위해 강의를 들었고 막상 듣기 시작하니 어느덧 1시간, 2시간 이상을 수월하게 공부할 수 있었다. 평소와는 달리 &amp;lsquo;미션&amp;rsquo;이 있었던 점도 소소한 즐거움이었다. 같이 공부한다는 것은, 혼자서 달성하기 힘든 목표를 함께 달성해나가는 과정이라는 걸 알게 되었다. 다음에는 공부하고 싶은 주제 하나를 잡고 스터디를 해보고 싶다. 같은 주제에 대해 다른 사람들은 어떻게 생각하는 지 배우면서, 더욱 이해를 넓혀나갈 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;노션에 독서 리스트를 만든 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성인이 된 이후로 독서는 항상 내 (희망 사항으로) 루틴에 포함되어 있었다. 마냥 열심히만 읽었는데 머리에 남는 게 많이 없었다. 그래서 작년부터 책을 읽으면 꼭 한,두 문단씩이라도 독후감을 남겨두었다. 그러다보니 좀 더 체계적으로 읽을 수 없을까? 하는 고민이 들었다. 우연한 계기로 노션 활용법을 알려주는 온라인 컨퍼런스를 듣게 되었고, 예시로 독서 리스트를 설명해주셨다. 나에게 딱 맞는 해결책이었고, 그 덕분에 여태껏 몇 권의 책을 읽었는지, 어떤 관심사를 가지고 책을 읽었는지(읽고 있는지) 등을 남겨둘 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 총 14권의 책을 읽었는데, 읽다가 만 책도 3권 있다. 첫 번째 프로젝트를 마친 직후에 많이 읽었고 이후로는 드문드문 몇 권씩 읽었다. 괜찮았던 책은 &lt;a href=&quot;https://seandailytech.tistory.com/27&quot;&gt;작년과 같이&lt;/a&gt; 블로그에도 후기를 남겨둬야겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1753&quot; data-origin-height=&quot;731&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TtF1d/dJMcagRFKiF/0FmgsTKgYkSAQNqAHBQKLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TtF1d/dJMcagRFKiF/0FmgsTKgYkSAQNqAHBQKLk/img.png&quot; data-alt=&quot;나의 2025년 독서 리스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TtF1d/dJMcagRFKiF/0FmgsTKgYkSAQNqAHBQKLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTtF1d%2FdJMcagRFKiF%2F0FmgsTKgYkSAQNqAHBQKLk%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;1753&quot; height=&quot;731&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1753&quot; data-origin-height=&quot;731&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;나의 2025년 독서 리스트&lt;/figcaption&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;2025년에 아쉬웠던 점 3가지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기술적인 숙련도를 충분히 올리지 못했던 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 해동안 여러 업무를 하면서 기술을 경험해 볼 수 있었다. 프론트엔드, 백엔드, 인증, 파이썬 SDK, 도커, 쿠버네티스 등등 일을 시작한 지 1년 반이 된 상태에서 다루기에는 너무 넓은 범위였던 것 같기도 하다. 그 때문인지 관련된 기술을 활용할 줄은 알게 되었어도 숙달하기에는 시간이 부족했다. 좀 더 깊게 파봐야지 싶으면 다른 일이 생기고, 그걸 해결하면 또 다른 일이 생기는 패턴이 반복되었다.&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;지금 돌이켜보면 시간은 한정되어 있기 때문에, 깊이와 범위에서 trade-off가 있을 수밖에 없었다. 흔히 언급되는 T자형 경력 관리에서, 맨 위의 가로축을 다졌다고 생각하자. 앞으로 Java 기반의 백엔드 프로그래밍에 전문성을 쌓고 그걸 기반으로 나머지 분야들도 깊이를 더하려고 한다. 사실 요새는 AI 툴이 너무 잘 되어있기 때문에 각 분야들의 숙련도도 빠르게 올릴 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;LLM에 관심을 덜 가졌던 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 세 번째 프로젝트를 마치고, 회사에 돌아와 2주 정도 LLM 및 MS Azure 활용 교육을 이수했다. Langchain, Langgraph, Langsmith와 같은 오픈소스부터 시작해서 LLM 성능 평가, MCP, AI Agent와 같은 최근의 심화된 이슈까지 정리했다. 그런 다음 MS Azure 플랫폼에서 어떻게 쉽게 구현할 수 있는지 배우고, 그걸 기반으로 토이 프로젝트를 직접 만들어보았다.&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;교육을 들으면서 LLM 기술이 하루가 다르게 발전하고 있음을 알게 되었다. 그리고 &lt;a href=&quot;https://github.com/Woonggss/llm-poc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;토이 프로젝트&lt;/a&gt;를 만들면서, 연습할 겸 VSCode에 Codex를 붙여서 바이브 코딩도 직접 해봤다. 완전 자동화는 불가능하고, 나도 기술을 잘 알아야 바이브 코딩도 잘할 수 있음을 알게 되었다. 어느 순간부터는 에이전트가 작성해주는 속도를 따라가지 못해서 코드를 충분히 검토하지 못했고, 결국 처음부터 다시 구현했던 적도 있었다.&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일 만에 작동하는 PoC를 만들 수 있었다. 이런 방식에 좀 더 익숙해지고, Multi Agent와 같이 여러 에이전트를 돌려서 협업하게 만들면 한 명의 개인이 할 수 있는 업무의 양과 범위가 무척 커질 것이라는 생각도 들었다. 또한 기술을 학습하는 데에도 LLM은 무척 유용하다. 구글애서 내놓은 &lt;a href=&quot;https://notebooklm.google/&quot;&gt;NotebookLM&lt;/a&gt;은 아예 학습에 특화된 AI 서비스로, 학습할 내용을 넣으면 해당 내용을 요약하고 그 내용을 기반으로 질의/응답을 하거나 학습용 문제를 출제해주는 등의 기능도 있다.&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;최근 들어 개발자의 핵심 역량은 How(어떻게 문제를 해결할 것인지)를 잘하는 것에서 What/Why(무슨 문제를 해결할 것인지, 왜 해야 하는지)를 정의하는 것으로 옮겨가고 있다고 한다. 어차피 How는 AI가 잘 해주기 때문이다. 여태껏 ChatGPT를 활용하면서 주어진 문제를 빠르게 해결하는 데에 만족했다면, 내년에는 하루가 다르게 발전하는 LLM의 트렌드를 따라가면서 어떻게 하면 생산성을 끌어올릴 수 있을 지 적극적으로 고민해 봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;자기관리에 소홀했던 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 시간이 갈수록 점점 자기관리에 소홀했던 것 같다. 먼저 운동을 하는 빈도가 줄었다. 작년에 취업을 한 뒤에 다시 복싱을 꾸준히 했었는데, 올해 들어 프로젝트를 나가기 시작하면서 제대로 하지 못하고 결국 그만두게 되었다. 대신 헬스장을 등록해서 러닝과 턱걸이라도 해오고 있었는데, 그마저도 점점 빈도가 줄어들었다. 요새는 주 1회 정도만 간간히 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;식단 관리도 마찬가지이다. 먹는 음식의 양과 질을 적절하게 관리하지 못했던 것 같다. 체중이 늘었다. 음주도 그렇다. 취준을 시작하면서 술을 끊었었고, 취업하고 나서도 좋은 컨디션을 유지하기 위해 가볍게만 마셔왔다. 언제부턴가 한 잔씩 늘더니 요새는 의식하지 않고 술을 마시고 있다. 그래도 평일에는 주량을 조절하지만, 건강을 생각하면 다음 날이 휴일이더라도 적당히 마시는 게 좋을 것 같다. 매일 루틴인 명상과 감사 일기 작성도 언젠가부터는 그냥 습관처럼 별 생각 없이 하고 있다. 명상 가이드 내용이 귀에 잘 들어오지 않고, 감사 일기에 쓰는 내용도 점점 매일 별다를 게 없어지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에 적응하고, 생활이 안정되면서 자연스레 긴장이 풀어졌던 것 같다. 업무에 집중했으니 퇴근하면 편히 쉬어야 한다고 자기합리화를 하기도 했다. 마음이 편안한 상태인 것은 무척 좋은 일이지만 그러기 위해서는 절제가 반드시 필요하다. 건강한 몸과 마음은 그냥 얻어지는 것이 아님을 늘 잊지 말자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;나의 2026년 키워드 : 몰입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내년에는 올해보다도 더욱 몰입하는 한 해가 되고 싶다. 프로젝트를 나가더라도 나의 생활을 놓지 않고 유지하려면, 주어진 시간 안에 더 많은 일을 헤야할 것 같다. 할 일들의 우선 순위를 설정하고, 각각의 일들에 집중해서 빠르고 완성도 있게 처리할 수 있도록 노력해야겠다.&lt;/p&gt;</description>
      <category>회고</category>
      <category>2025년 회고</category>
      <category>개발자</category>
      <category>소프트웨어 개발자</category>
      <category>연말 회고</category>
      <category>회고</category>
      <author>devsean</author>
      <guid isPermaLink="true">https://seandailytech.tistory.com/45</guid>
      <comments>https://seandailytech.tistory.com/45#entry45comment</comments>
      <pubDate>Mon, 29 Dec 2025 21:24:08 +0900</pubDate>
    </item>
    <item>
      <title>9. 추석 연휴 스터디 챌린지와 갈망의 아궁이</title>
      <link>https://seandailytech.tistory.com/44</link>
      <description>&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://seandailytech.tistory.com/43&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;저번 프로젝트&lt;/a&gt;를 마치면서, 개발을 잘하고 싶다는 욕심이 생겼다. 마침 인프런에서 추석 연휴 기간에 &lt;a href=&quot;https://www.inflearn.com/challenge/x27%ED%96%A5%EB%A1%9Cx27-%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%B6%94%EC%84%9D-%EC%99%84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스터디 챌린지&lt;/a&gt;를 진행하고 있었다. 인프런의 CTO이신 향로님이 주최하는 챌린지로, 매일 최소 한 강씩 듣고 스터디 인증을 하는 것이 미션이었다. 때마침 듣고 싶은 강의(토비의 스프링 6 - 이해와 원리)도 있었고, 다른 사람들과 함께 공부하는 것은 어떤 느낌일지도 궁금했다. 챌린지를 하면서 연휴 내에 12시간 강의를 완강하였다. 느낀 점을 적어봤다.&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;&amp;lsquo;함께 공부하기&amp;rsquo;의 효용성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챌린지를 하면서 &amp;lsquo;함께 공부하기&amp;rsquo;의 효용성을 크게 느낄 수 있었다. 먼저 &lt;b&gt;최소한의 강제성&lt;/b&gt;이 부여되니 꾸준히 해나갈 수 있었다. 챌린지의 완주 조건은 매일 한 강씩 강의를 듣는 것이었다. 긴 연휴동안 공부하기 싫은 날도 있었지만, 그럴 때마다 챌린지 조건을 지키기 위해 일단 한 강을 들었고 막상 듣기 시작하니까 다음 강의도 듣고 싶어져서 공부를 지속할 수 있었다. 변화하고 싶다면 의지력만 믿지 말고, 환경을 셋팅해주어야 함을 느낄 수 있었다. 그리고 함께하는 사람들이 있다는 &lt;b&gt;사실만으로도 큰&lt;/b&gt; &lt;b&gt;동기부여&lt;/b&gt;가 되었다. 챌린지를 주최한 향로님께서 연휴 기간동안 3번의 라이브를 열어주셨다. 그 때 챌린지 참여하는 분들이 모였는데 그 자체로도 힘이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보면 개발자로 일하면서 주중에는 일을 하고, 저녁 혹은 주말에 개인 공부를 한다는 건 쉬운 일은 아니다. 올해 들어 프로젝트에서 사용한 기술들을 꾸준히 공부하고자 마음먹고 해왔음에도, 많은 내용들을 다루지는 못했다. 블로그에 포스팅하는 주기도 점점 길어졌다. 이번처럼 같은 목표를 가진 사람들과 함께 공부하면서 강제성을 부여하고, 학습한 내용을 공유하면서 서로에게 인사이트를 줄 수 있다면 훨씬 유익한 학습이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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://www.slideshare.net/slideshow/ss-6097436/6097436#1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;갈망의 아궁이&lt;/a&gt;에 대한 발표를 소개해주셨다. 갈망이란 뭔가를 간절히 원함으로써, 실제 행동으로 이어지게 만드는 추진력이다. 성취, 취미 생활, 배움, 내가 좋아하는 물건 등 무엇이든 갈망은 우리를 살아있게 만든다. 우리는 각자 갈망의 불씨를 담고 있는 아궁이를 가지고 있다. 앞서 말한 갈망들은 갈망의 아궁이에서 불씨를 꺼트리지 않고, 타오르게 하는 연료가 되어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지에 대해서만 끊임없이 갈망할 수는 없다. 언젠가는 고갈되는 순간이 찾아오고, 그 때 갈망의 불씨를 살려줄 다른 무언가를 넣어주어야 한다. 요즘 들어서 무기력하다는 생각이 들었다. 뭐든지 새롭고 희망적이었던 지난 시간이, 익숙해지고 또 보이지 않던 현실이 보이면서 조금씩 불씨가 사그라들었던 것 같다. 갈망하는 무언가를 점점 잊은 채로, 타성에 젖었기 떄문이었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 스터디는 갈망의 아궁이에 &lt;b&gt;사그라들던 불씨를 지펴주는 순간&lt;/b&gt;이었다. 스터디를 시작하면서 연휴에 시간을 쪼개서 완강을 하고 싶다는 갈망이 생겼고, 완강을 하고 나니까 자연스레 다른 것들도 해보고 싶다는 생각이 든다. &lt;b&gt;갈망의 아궁이에 다른 무언가를 더 넣어주고 싶어졌다.&lt;/b&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;figure id=&quot;og_1760530673600&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - Woonggss/toby-spring6-study: [토비의 스프링 6 - 이해와 원리] 강의를 수강하면서 학습했던 소스&quot; data-og-description=&quot;[토비의 스프링 6 - 이해와 원리] 강의를 수강하면서 학습했던 소스코드입니다. Contribute to Woonggss/toby-spring6-study development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Woonggss/toby-spring6-study&quot; data-og-url=&quot;https://github.com/Woonggss/toby-spring6-study&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cJdfyb/hyZLb0Vnlx/vVkuk1TxaDdTpusyc4gs21/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/vcCfu/hyZLpemeO7/U48KSxwesQ1cqkx6oAYKXk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Woonggss/toby-spring6-study&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Woonggss/toby-spring6-study&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cJdfyb/hyZLb0Vnlx/vVkuk1TxaDdTpusyc4gs21/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/vcCfu/hyZLpemeO7/U48KSxwesQ1cqkx6oAYKXk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - Woonggss/toby-spring6-study: [토비의 스프링 6 - 이해와 원리] 강의를 수강하면서 학습했던 소스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[토비의 스프링 6 - 이해와 원리] 강의를 수강하면서 학습했던 소스코드입니다. Contribute to Woonggss/toby-spring6-study development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>갈망</category>
      <category>갈망의 아궁이</category>
      <category>개발</category>
      <category>스터디</category>
      <category>인프런</category>
      <category>챌린지</category>
      <category>토비의 스프링 6</category>
      <category>회고</category>
      <author>devsean</author>
      <guid isPermaLink="true">https://seandailytech.tistory.com/44</guid>
      <comments>https://seandailytech.tistory.com/44#entry44comment</comments>
      <pubDate>Wed, 15 Oct 2025 21:19:08 +0900</pubDate>
    </item>
    <item>
      <title>8. 좋은 개발자에 대해 생각했던, 세 번째 프로젝트 회고</title>
      <link>https://seandailytech.tistory.com/43</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 프로젝트를 마치고 바로 3개월 간 투입되었다. 이번 프로젝트에서는 기존에 납품된 솔루션에 대한 On-Site 기술 지원 업무를 수행했다. 기존에 구축되어 있는 솔루션에 SSO 연동 기능을 개발하고, 솔루션 관련 문의사항에 대해 답변하고 솔루션 사용 중 발생하는 에러를 해결했다. 프로젝트를 수행하면서 점점 자신감이 붙고 있다. 어떤 개발자가 되고 싶은지 나름의 답을 찾을 수 있었고, 더 잘하고 싶다는 욕심이 커졌다.&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;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 할 때마다, &amp;lsquo;저 분 잘한다&amp;rsquo;라고 느끼는 개발자 분들이 꼭 한 분씩은 계셨다. 이번에도 마찬가지였다. 문득 &amp;lsquo;왜 나는 저 분이 잘한다고 느낄까?&amp;rsquo; 생각해봤다. 그 분은 &lt;b&gt;본인이 생각한 내용을 타인이 이해하기 쉽게 잘 설명했다.&lt;/b&gt; 프로젝트를 수행하면서 내가 아는 내용을 누군가에게 적절하게 설명하는 일이 얼마나 중요한 일인가를 크게 느낄 수 있었다. 기술적인 배경이 많지 않은 고객이 상황을 잘 이해하도록 설명하거나, 내가 다른 개발자에게 도움을 청하거나, 상사에게 진척 상황을 보고할 때 등 &lt;b&gt;많은 상황에서 &amp;lsquo;잘 설명하는 것&amp;rsquo;은 중요했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 능력을 갖추기 위해, 다음의 2가지가 갖추어져야 한다고 생각한다. 첫째는 &lt;b&gt;설명하려는 주제에 대한 명확한 이해&lt;/b&gt;이다. 어떤 것도 명확하게 이해하지 않으면 제대로 설명할 수 없다. 나는 기술 학습을 하면서 공부한 내용을 글로 정리해두는데, 그렇게 하면 내가 어디까지 알고 어디까지 모르는지가 명확해진다. 적어도 스스로 납득할 수 있을 만큼은 이해하고 있어야 자신있게 남에게 설명할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 &lt;b&gt;설명을 듣는 사람의 눈높이 파악&lt;/b&gt;이다. 설령 나보다 경험이 풍부한 개발자여도 내가 진행한 영역에 대해서는 경험이 없을 수도 있고, 내가 설명하게 될 사람이 개발자가 아닌 경우도 부지기수이다. 그렇기 때문에 대화를 하며 그 사람의 눈높이를 파악하고, 이해할 수 있게끔 설명하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 기술력을 갖추기 위해 들이는 노력과 시간이 개발자의 역량을 크게 좌우했다. 학습과 업무를 도와주는 AI 툴들이 많아지면서, 그러한 노력과 시간을 크게 줄일 수 있게 되었다. 생각해보면 AI에게 도움을 받는 과정도 본질적으로는 구하고자 하는 지식이 무엇인지 잘 설명하는 것이다. 그렇기 때문에, &lt;b&gt;(AI를 포함하여) 같이 일하는 당사자들에게 잘 설명하는 능력은 대체되지 않는 개발자의 중요한 요건이 되었다.&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;SSO 연동 개발을 하면서 다른 회사 개발자분께 도움을 받았다. 업무를 시작하면서 팀의 상사분으로부터 개략적인 업무에 대해서, 그리고 다른 사이트에는 어떻게 구축되어 있는 지에 대해서 설명을 들었다. 아쉽게도 다른 방식으로 SSO를 구현하고 있었고, 현재 사이트에 구축된 솔루션의 인프라 환경에 대해 문의하는 것 외에는 사실상 혼자서 개발해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 참고할 수 있는 가이드 문서도 함께 전달받았다. 가이드 문서는 인증 솔루션을 구축한 업체에서 작성한 문서가 있었고, 기존에 구축을 해보셨던 다른 회사의 개발자분께서 추가로 작성해주신 문서가 있었다. 해당 개발자 분이 본인이 구현했던 내용을, 하나씩 단계별로 잘 정리해주셨어서 나도 따라하는 데에 무리가 없었다. 막히는 부분이 있었을 때 직접 찾아가서 여쭤보기도 했다. 그럴 때도 같이 문제를 해결해주신 덕분에, 무사히 잘 해낼 수 있었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 회사 직원도 아닌데, 내가 궁금한 점이 있을 때 잘 답변해주신 것은 다시 생각해도 정말 감사한 일이다. 개발자들은 본인이 관심 있는 기술 주제에 대해 함께 스터디를 하거나 컨퍼런스를 열어 의견을 교류한다. 타 직종에 비해 자연스럽게 서로의 지식을 공유하고 함께 성장하는 문화가 잘 잡혀있다. 작년 파이콘에 참석해서 연사분들의 발표를 들으면서도 많이 배웠다. 이번에 업무를 하면서도, 함께 문제를 해결해나가는 개발자 문화를 경험할 수 있었다. &lt;b&gt;나도 실력을 갖추고 다른 개발자에게 도움을 줄 수 있는 개발자가 되고 싶다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 잘하고 싶은 욕심&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번의 프로젝트를 경험하면서, 개발 업무에 자신감이 붙었다. 그러면서 어떻게 하면 더 잘할 수 있을 지 고민할 수 있는 여유 또한 생겼다. &lt;b&gt;좋은 코드를 작성하고 싶고, 더 품질이 좋은 소프트웨어를 만들고 싶다.&lt;/b&gt; 이런 욕심이 여태까지는 좀 더 세속적인 동기(돈, 안정성, 사회적인 인정 등등)에서 출발했다면, 이번에는 좀 더 원초적인 동기(호기심, 완성도 높은 작품, 보람 등등)로부터 출발했다. &lt;b&gt;여러 주제들을 학습하고, 직접 코드로 작성해보고, 가능하면 내 업무에도 적용해보면서 더 잘하는 개발자가 되고 싶다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관련 포스팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://seandailytech.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;7. 짧지만 임팩트 있었던 Python 개발기, 두 번째 프로젝트 회고&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #ffffff; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://seandailytech.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SSO(Single Sign-On)&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #ffffff; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://seandailytech.tistory.com/37&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring Security] 필터 단에서 예외 발생 시 적절한 응답 보내주기&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>개발자</category>
      <category>소프트웨어 장인정신</category>
      <category>좋은 개발자</category>
      <category>회고</category>
      <author>devsean</author>
      <guid isPermaLink="true">https://seandailytech.tistory.com/43</guid>
      <comments>https://seandailytech.tistory.com/43#entry43comment</comments>
      <pubDate>Fri, 3 Oct 2025 19:39:40 +0900</pubDate>
    </item>
    <item>
      <title>조회 성능을 위한 DB Index(feat.MySQL)</title>
      <link>https://seandailytech.tistory.com/42</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 웹 어플리케이션을 구축하는 데 있어 DB 입출력 작업은 필수적이다. 그러므로 서버 성능의 관점에서, 클라이언트의 요청에 신속하게 대응하기 위해서는 DB 성능을 함께 고려해야 한다. DB 성능에 있어 인덱스는 늘 빠지지 않고 등장하는 주제이다. 관련하여 여러 개념들을 들어왔는데, 한 번 정리해보고자 한다. 널리 활용되는 RDBMS인 MySQL을 기준으로 작성했다.&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;Index의 정의와 필요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index란 DB의 Table에 대한 동작 속도를 높여주는 자료구조이다. 책을 읽을 때 활용하는 &lt;b&gt;목차&lt;/b&gt;와 같다. 목차를 참고하여, 원하는 정보를 더욱 빠르게 얻을 수 있다. 웹 어플리케이션 동작 시에는 DB Table에 대한 조회가 빈번하게 일어나고, 대부분 필터링을 적용한다. 이 때 DB에서는 조건에 맞는 데이터를 조회하기 위해 모든 테이블을 조회(Full-Scan)하게 되는데, 연산의 관점에서 보면 낭비가 발생한다. Index를 활용하여, 이러한 낭비를 줄일 수 있다.&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;Index 동작 원리(B+Tree 자료구조)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index는 어떻게 조회 성능을 비약적으로 높일 수 있을까? 내부적으로 &lt;b&gt;B+Tree 자료구조&lt;/b&gt;를 사용하기 때문에 가능하다. B+Tree 자료구조는 바이너리 트리랑 비슷한 자료구조로, 차이점은 Children 노드가 여러 개이다. Key 값으로 정렬되어 있어 Binary Search를 사용하기에 조회 시 시간 복잡도를 비약적으로 향상시킬 수 있다. 데이터는 Leaf 노드에만 저장되어 있어 Key를 탐색하여 해당하는 데이터를 조회해온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imageeee.png&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnz02z/btsP4v3oT9t/xeRD37sKmPkm9mT6PSRb8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnz02z/btsP4v3oT9t/xeRD37sKmPkm9mT6PSRb8k/img.png&quot; data-alt=&quot;단순한 형태의 B+Tree(출처 : 위키피디아)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnz02z/btsP4v3oT9t/xeRD37sKmPkm9mT6PSRb8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcnz02z%2FbtsP4v3oT9t%2FxeRD37sKmPkm9mT6PSRb8k%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;1121&quot; height=&quot;516&quot; data-filename=&quot;imageeee.png&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;단순한 형태의 B+Tree(출처 : 위키피디아)&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;데이터를 조회할 때, B+Tree 구조에서 어떻게 읽게 되는지 살펴보자. 각 노드는 디스크에 저장되어 있으므로 주소값을 가진다. 루트에서부터 Key 값을 찾기 시작하는데, 노드에 저장된 Key들을 기준으로 작은 경우, 사잇값(같은 경우를 포함), 큰 경우를 따져서 다음 노드로 내려가면서 탐색한다. 노드 내부에서 Binary Search를 적용한다.&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;시간 복잡도를 계산해보자. N개의 데이터가 B+Tree에 저장되어 있다고 가정한다. 포인터는 주소를 가리키기 때문에 O(1)이며, 각 노드별로 포인터의 갯수(차수)만큼 조회해야 하는 비용으로 O(logM)이 들지만, M은 디스크 블록 크기에 따라 고정되어 있으므로 상수 취급한다. 그러면 트리의 높이만큼 탐색해야 하는데, 이는 m을 밑으로 하는 log N이며 근사치로 O(logN)이다. 그러므로 O(logN)의 시간 복잡도를 가지게 되며, Full-Scan을 할 때의 시간 복잡도인 O(N)보다 비약적으로 향상된 성능을 가지게 된다.&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;참고로 각 노드는 sibling(형제, 같은 층위에 있는 노드)로 연결할 수 있는 포인터가 존재한다. 이러한 특징은 Range 쿼리를 수행할 때 유용하게 활용될 수 있다. B+Tree 자료구조에 삽입, 수정, 삭제 등의 연산을 할 때는 복잡한 연산이 필요하다. 참고한 영상에서 쉽게 풀어서 설명하고 있다.&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;Index의 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index의 종류는 다음과 같다. 대표적인 것들만 작성했고, 이외에도 여러 Index가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Clustered Index&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 당 한 개만 생성되는 인덱스로, 행 데이터를 인덱스로 지정한 열에 맞추어 자동 정렬한다. &lt;b&gt;Primary Key 설정 시 자동으로 생성&lt;/b&gt;된다. 모든 보조 인덱스(Non-Clustered Index)는 PK를 포함하며, PK가 커질수록 자연히 보조 인덱스의 크기도 커진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Non-Clustered Index&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Clustered Index 이외의 인덱스&lt;/b&gt;를 의미한다. Clustered Index와 달리 여러 개를 가질 수 있으며, 인덱스 순서와 물리적 순서가 불일치한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Single Index&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하나의 컬럼&lt;/b&gt;으로 구성된 Index를 의미한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Composite Index&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 개의 컬럼 조합&lt;/b&gt;으로 생성된 Index를 의미한다.&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;Index 설계 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Index를 과도하게 사용한다면 되려 성능이 악화될 수 있다.&lt;/b&gt; Index도 디스크에 저장되기 때문에 공간을 낭비하게 될 수도 있고, 조회가 아닌 변경(수정,삭제,추가) 연산의 경우 Index에도 반영해주어야 하기 때문에 시간을 낭비(불필요한 연산)하게 될 수도 있다. &lt;b&gt;그러므로 적절한 설계 전략을 세워야 한다.&lt;/b&gt; &amp;lt;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식&amp;gt; 책을 주로 참고하였으며, 마지막 PK 전략의 경우 프로젝트를 하며 늘 적용했던 이유가 궁금해서 한 번 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;조회 패턴에 맞게 설계하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자주 날아오는 조회 쿼리가 어떤 패턴을 가지고 있는지 파악&lt;/b&gt;해야 한다. 예를 들어 게시판 서비스에서 &amp;lsquo;마이페이지&amp;rsquo; 기능이 있고 &amp;lsquo;내가 작성한 글 조회하기&amp;rsquo; 기능이 있는 경우, 어떤 유저가 작성했는지를 기준으로(user_id 라던지) 인덱스를 걸어야 할 것이다. 비즈니스 로직을 잘 이해하고 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;선택도를 고려하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선택도가 높은, 칼럼 데이터의 경우의 수가 다양한 컬럼일수록 인덱스의 효율이 높다.&lt;/b&gt; 태어난 년도와 성별을 기준으로 조회하는 경우를 생각해보자. gender 칼럼이 M, F, N 3개의 값 중 하나를 갖고, 전체 회원 데이터 중 M이 50만 개, F가 50만 개, N이 천 개라면 gender를 인덱스로 걸었더라도 여전히 50만개의 데이터를 확인해야 한다. 그러나 birthyear는 정수형이고 더 많은 경우의 수를 가지게 되므로 좀 더 효율이 높을 것이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;select * from member where gender = 'F' and birthyear = 1900;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;커버링 인덱스 활용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커버링 인덱스란 &lt;b&gt;특정 쿼리를 실행하는 데 필요한 칼럼을 모두 포함하는 인덱스&lt;/b&gt;를 의미한다. 커버링 인덱스가 지정되어 있으면 &lt;b&gt;조회할 때 Clustered Index를 통해 실제 테이블에 접근하지 않고 바로 데이터를 조회&lt;/b&gt;하므로 성능 상으로 이점을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;create index test_idx on tbl (a,b);
select a,b from tbl where a='3' and b='4';
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;PK(Clustered Index)로 정수형, AUTO INCREMENT 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 정수형으로 설정하고 AUTO INCREMENT를 주는데, &lt;b&gt;저장 공간의 이점&lt;/b&gt;도 있고 보조 인덱스를 사용하면 내부적으로 데이터 조회 시 해당하는 PK를 가져와서 &lt;b&gt;PK 기준으로 한 번 더 조회를 하기 때문에 정수형으로 설정&lt;/b&gt;하는 것이 성능에 유리하다. 데이터 크기가 작을수록 트리 높이가 낮아지기 때문이다. 또한, AUTO INCREMENT를 주면 &lt;b&gt;새로운 데이터가 생성되어도&lt;/b&gt; &lt;b&gt;인덱스 상에서 무조건 맨 뒤에 삽입&lt;/b&gt;되기 때문에 인덱스를 조정하는 연산을 보다 단순하게 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255),
    name VARCHAR(100)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Index 사용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index를 간단하게 사용하고, 옵티마이저를 통해 성능 개선 여부를 확인하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스키마&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PRIMARY KEY는 InnoDB가 자동으로 인덱스 생성&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255),
    name VARCHAR(100)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인덱스 확인 - PK&lt;/h4&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;SHOW INDEX from Users;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| users |          0 | PRIMARY  |            1 | id          | A         |           0 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
1 row in set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인덱스 성능 비교를 위한 데이터 주입&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저 Selection vs 고 Selection&lt;/li&gt;
&lt;li&gt;&lt;b&gt;email&lt;/b&gt; &amp;rarr; 모든 행이 서로 다른 값 &amp;rarr; &lt;b&gt;고 Selection&lt;/b&gt; (인덱스 효과 큼)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;name&lt;/b&gt; &amp;rarr; 값 종류가 적고 반복됨 (예: 'Alice', 'Bob', 'Charlie', &amp;lsquo;David&amp;rsquo;) &amp;rarr; &lt;b&gt;저 Selection&lt;/b&gt; (인덱스 효과 낮음)&lt;/li&gt;
&lt;li&gt;총 20개의 행 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;INSERT INTO users (email, name) VALUES
-- name = 'Alice'
('alice001@example.com', 'Alice'),
('alice002@example.com', 'Alice'),
('alice003@example.com', 'Alice'),
('alice004@example.com', 'Alice'),
('alice005@example.com', 'Alice'),

-- name = 'Bob'
('bob001@example.com', 'Bob'),
('bob002@example.com', 'Bob'),
('bob003@example.com', 'Bob'),
('bob004@example.com', 'Bob'),
('bob005@example.com', 'Bob'),

-- name = 'Charlie'
('charlie001@example.com', 'Charlie'),
('charlie002@example.com', 'Charlie'),
('charlie003@example.com', 'Charlie'),
('charlie004@example.com', 'Charlie'),
('charlie005@example.com', 'Charlie'),

-- name = 'David'
('david001@example.com', 'David'),
('david002@example.com', 'David'),
('david003@example.com', 'David'),
('david004@example.com', 'David'),
('david005@example.com', 'David');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인덱스 설정 및 테스트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봐야할 rows의 갯수가 1개, 5개로 차이가 있다. rows는 EXPLAIN 했을 때 옵티마이저가 확인할 것으로 예상되는 row의 숫자이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 고Selection 인덱스 테스트
CREATE INDEX idx_users_email ON users(email);

EXPLAIN SELECT * FROM users WHERE email = 'bob003@example.com';

+------+-------------+-------+------+-----------------+-----------------+---------+-------+------+-----------------------+
| id   | select_type | table | type | possible_keys   | key             | key_len | ref   | rows | Extra                 |
+------+-------------+-------+------+-----------------+-----------------+---------+-------+------+-----------------------+
|    1 | SIMPLE      | users | ref  | idx_users_email | idx_users_email | 1023    | const | 1    | Using index condition |
+------+-------------+-------+------+-----------------+-----------------+---------+-------+------+-----------------------+

-- 저Selection 인덱스 테스트
CREATE INDEX idx_users_name ON users(name);

EXPLAIN SELECT * FROM users WHERE name = 'Bob';

+------+-------------+-------+------+----------------+----------------+---------+-------+------+-----------------------+
| id   | select_type | table | type | possible_keys  | key            | key_len | ref   | rows | Extra                 |
+------+-------------+-------+------+----------------+----------------+---------+-------+------+-----------------------+
|    1 | SIMPLE      | users | ref  | idx_users_name | idx_users_name | 403     | const | 5    | Using index condition |
+------+-------------+-------+------+----------------+----------------+---------+-------+------+-----------------------+

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INDEX를 DROP하고 확인하면, rows가 20개가 나온다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DROP INDEX idx_users_email ON USERS;
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

EXPLAIN SELECT * FROM USERS WHERE email = 'bob003@example.com';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | USERS | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   20 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=yLe7_3cGSeU&quot;&gt;데이터베이스 인덱싱 자료구조 | B+ Tree 완전정복 | DB 의 데이터 저장 방법&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=yLe7_3cGSeU&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/ca1who/hyZzDdQEXO/oK6WMu3F72xQ3WVUm1slC0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=1172_52_1220_104,https://scrap.kakaocdn.net/dn/HETgB/hyZC4ab5M1/NZVCt16bKDuwuK8x4VJigk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=1172_52_1220_104&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;데이터베이스 인덱싱 자료구조 | B+ Tree 완전정복 | DB 의 데이터 저장 방법&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/yLe7_3cGSeU&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure id=&quot;og_1756010221444&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;1. 커버링 인덱스 (기본 지식 / WHERE / GROUP BY)&quot; data-og-description=&quot;일반적으로 인덱스를 설계한다고하면 WHERE절에 대한 인덱스 설계를 이야기하지만 사실 WHERE뿐만 아니라 쿼리 전체에 대해 인덱스 설계가 필요합니다. 인덱스의 전반적인 내용은 이전 포스팅을 &quot; data-og-host=&quot;jojoldu.tistory.com&quot; data-og-source-url=&quot;https://jojoldu.tistory.com/476&quot; data-og-url=&quot;https://jojoldu.tistory.com/476&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bGjAyf/hyZC6slH6J/5E4jEgMMhVdZZRwivs8MPk/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/bo3TVw/hyZC3PVkv4/b0KK3EVtIhEZWAVBD6kkj0/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/bBc4rN/hyZDZfciA7/0kb4owhy2g8ZkPZIphA2Jk/img.png?width=2220&amp;amp;height=1200&amp;amp;face=0_0_2220_1200&quot;&gt;&lt;a href=&quot;https://jojoldu.tistory.com/476&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jojoldu.tistory.com/476&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bGjAyf/hyZC6slH6J/5E4jEgMMhVdZZRwivs8MPk/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/bo3TVw/hyZC3PVkv4/b0KK3EVtIhEZWAVBD6kkj0/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/bBc4rN/hyZDZfciA7/0kb4owhy2g8ZkPZIphA2Jk/img.png?width=2220&amp;amp;height=1200&amp;amp;face=0_0_2220_1200');&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;1. 커버링 인덱스 (기본 지식 / WHERE / GROUP BY)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;일반적으로 인덱스를 설계한다고하면 WHERE절에 대한 인덱스 설계를 이야기하지만 사실 WHERE뿐만 아니라 쿼리 전체에 대해 인덱스 설계가 필요합니다. 인덱스의 전반적인 내용은 이전 포스팅을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jojoldu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 CHATPER 3(2025, 최범균)&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756010249845&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;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 | 최범균 - 교보문고&quot; data-og-description=&quot;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 | 실무에서 자주 겪는 다양한 문제를 효과적으로 해결하는 법서비스 환경에서는 커넥션을 닫지 않아 서버가 멈추고 외부 API의 지연이 전체 &quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000216376461&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000216376461&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bqeYpi/hyZDaBtumG/Rkp0vvX9OwLTeCgPvzW01K/img.jpg?width=458&amp;amp;height=588&amp;amp;face=0_0_458_588,https://scrap.kakaocdn.net/dn/YXo8K/hyZCXvnsWo/g7pZcvjoC7VrAgjk5C6G4K/img.jpg?width=458&amp;amp;height=588&amp;amp;face=0_0_458_588,https://scrap.kakaocdn.net/dn/CRJq4/hyZDOdFOAO/G8J9kUwtAOqKfIE5L0YTKk/img.png?width=335&amp;amp;height=335&amp;amp;face=0_0_335_335&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000216376461&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000216376461&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bqeYpi/hyZDaBtumG/Rkp0vvX9OwLTeCgPvzW01K/img.jpg?width=458&amp;amp;height=588&amp;amp;face=0_0_458_588,https://scrap.kakaocdn.net/dn/YXo8K/hyZCXvnsWo/g7pZcvjoC7VrAgjk5C6G4K/img.jpg?width=458&amp;amp;height=588&amp;amp;face=0_0_458_588,https://scrap.kakaocdn.net/dn/CRJq4/hyZDOdFOAO/G8J9kUwtAOqKfIE5L0YTKk/img.png?width=335&amp;amp;height=335&amp;amp;face=0_0_335_335');&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;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 | 최범균 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 | 실무에서 자주 겪는 다양한 문제를 효과적으로 해결하는 법서비스 환경에서는 커넥션을 닫지 않아 서버가 멈추고 외부 API의 지연이 전체&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1756010420734&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;B+ 트리 - 위키백과, 우리 모두의 백과사전&quot; data-og-description=&quot;위키백과, 우리 모두의 백과사전. 단순한 B+ 트리의 예 B+ 트리(Quaternary Tree라고도 알려져 있음)는 컴퓨터 과학용어로, 키에 의해서 각각 식별되는 레코드의 효율적인 삽입, 검색과 삭제를 통해 정&quot; data-og-host=&quot;ko.wikipedia.org&quot; data-og-source-url=&quot;https://ko.wikipedia.org/wiki/B%2B_%ED%8A%B8%EB%A6%AC&quot; data-og-url=&quot;https://ko.wikipedia.org/wiki/B%2B_%ED%8A%B8%EB%A6%AC&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2ajXw/hyZDanWNkS/oLImHiO6x9ASoaFci2L3r1/img.png?width=1121&amp;amp;height=516&amp;amp;face=0_0_1121_516,https://scrap.kakaocdn.net/dn/kOWNZ/hyZzGBAiDm/FcO1feiDNYTqTl2HsHvecK/img.png?width=960&amp;amp;height=442&amp;amp;face=0_0_960_442&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/B%2B_%ED%8A%B8%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.wikipedia.org/wiki/B%2B_%ED%8A%B8%EB%A6%AC&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2ajXw/hyZDanWNkS/oLImHiO6x9ASoaFci2L3r1/img.png?width=1121&amp;amp;height=516&amp;amp;face=0_0_1121_516,https://scrap.kakaocdn.net/dn/kOWNZ/hyZzGBAiDm/FcO1feiDNYTqTl2HsHvecK/img.png?width=960&amp;amp;height=442&amp;amp;face=0_0_960_442');&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;B+ 트리 - 위키백과, 우리 모두의 백과사전&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;위키백과, 우리 모두의 백과사전. 단순한 B+ 트리의 예 B+ 트리(Quaternary Tree라고도 알려져 있음)는 컴퓨터 과학용어로, 키에 의해서 각각 식별되는 레코드의 효율적인 삽입, 검색과 삭제를 통해 정&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.wikipedia.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/설계</category>
      <category>B+Tree</category>
      <category>db</category>
      <category>index</category>
      <category>MySQL</category>
      <category>성능</category>
      <author>devsean</author>
      <guid isPermaLink="true">https://seandailytech.tistory.com/42</guid>
      <comments>https://seandailytech.tistory.com/42#entry42comment</comments>
      <pubDate>Sun, 24 Aug 2025 13:44:48 +0900</pubDate>
    </item>
    <item>
      <title>[Vue.js] 당장 필요한 만큼만 이해하기</title>
      <link>https://seandailytech.tistory.com/41</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무에서 프론트엔드 개발에 Vue.js 프레임워크를 활용하고 있다. 기존 소스코드를 빠르게 파악하기 위해 공식 문서를 읽으면서 필요한 개념들을 공부하였다. SPA의 정의와 Vue.js 프레임워크 소개, 가상 DOM을 활용하는 렌더링 메커니즘, 반응성, 컴포넌트, 라우팅까지 &amp;ldquo;당장 필요한 만큼만&amp;rdquo; 이해해보고자 한다. 그 밖에 생명 주기, 상태 관리 등 세부적인 내용은 공식 문서에 잘 정리가 되어있으므로 추후 개발하면서 필요할 때마다 참고할 수 있다.&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;SPA(Single Page Application)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPA를 용어 그대로 풀이하면, &lt;b&gt;하나의 페이지로 이루어진 어플리케이션&lt;/b&gt;이다. 많은 경우에 웹 어플리케이션은 사용자의 복잡한 상호작용을 지원할 필요가 있다. 하나의 화면 안에서도 사용자의 권한 여부에 따라 컨텐츠를 다르게 보여준다거나, 사용자의 상호작용(입력)에 따른 데이터 및 화면 변경이 복잡하게 수행될 수 있다. SPA에서는 하나의 페이지가 여러 개의 &lt;b&gt;컴포넌트&lt;/b&gt;로 구성되어 있으며, 각 컴포넌트는 자식 컴포넌트 여러 개로 구성될 수 있고 &lt;b&gt;상태&lt;/b&gt;에 따라 그 내용(구성,데이터,&amp;hellip;)이 바뀔 수 있다. &lt;b&gt;라우팅(경로 이동)&lt;/b&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;Vue.js?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue는 사용자 인터페이스(UI)를 구축하기 위한 Javascript 프레임워크이다. &lt;b&gt;컴포넌트 기반의 프로그래밍 모델&lt;/b&gt;을 제공하여 효율적으로 UI를 개발할 수 있다. Single Page Application으로써 널리 활용되며, 상황에 따라 SSR 프레임워크(Nuxt.js)로도 활용할 수도 있고, 별도의 빌드 없이 정적 HTML 파일을 꾸며주기도 하는 등 다양한 용도로 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서는 핵심 기능으로 &lt;b&gt;선언적 렌더링&lt;/b&gt;과 &lt;b&gt;반응성&lt;/b&gt;을 제시하고 있다. &lt;b&gt;선언적 렌더링&lt;/b&gt;이란 &lt;b&gt;Javascript 상태에 따라&lt;/b&gt; &lt;b&gt;HTML 출력을 선언적으로 기술&lt;/b&gt;할 수 있는 특성을 의미한다. HTML을 확장한 템플릿 문법을 활용하여 쉽게 기술할 수 있다. 한편, &lt;b&gt;반응성을 제공한다&lt;/b&gt;는 것은 &lt;b&gt;JavaScript&lt;/b&gt; &lt;b&gt;상태 변화를 자동으로 추적&lt;/b&gt;하고, 변화가 발생하면 &lt;b&gt;DOM을 효율적으로 업데이트&lt;/b&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;가상 DOM을 활용한 렌더링 메커니즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 Vue.js는 어떻게 화면을 그릴까(렌더링할까)? Vue의 각 컴포넌트는 &lt;b&gt;템플릿(template)&lt;/b&gt;으로 작성되며 이러한 템플릿들은 &lt;b&gt;HTML의 DOM으로 변환&lt;/b&gt;된다. 참고로 DOM이란 Document Object Model의 줄임말로, 브라우저가 HTML 문서를 메모리에 올려놓은 &amp;ldquo;트리 구조 객체 모델&amp;rdquo;을 의미한다. 웹 페이지의 구성 요소들을 프로그래밍 할 수 있는 인터페이스라고 보면 된다. Vue.js는 이러한 템플릿을 DOM으로 변환할 때, &lt;b&gt;가상의 DOM을 만들어서 메모리에 보관하고, 실제 DOM으로 변환해주게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 렌더링 과정 - &lt;b&gt;렌더 파이프라인&lt;/b&gt; - 을 좀 더 상세히 살펴보자. 크게 &lt;b&gt;컴파일 &amp;rarr; 마운트 &amp;rarr; 패치&lt;/b&gt; 3단계로 구성되며, 각 단계에 대한 설명은 공식 문서 설명이 잘 되어 있어서 그대로 가져왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (5).png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8fFib/btsP2JawuVv/Px3XlIxlp9C8Vr19FPqGMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8fFib/btsP2JawuVv/Px3XlIxlp9C8Vr19FPqGMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8fFib/btsP2JawuVv/Px3XlIxlp9C8Vr19FPqGMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8fFib%2FbtsP2JawuVv%2FPx3XlIxlp9C8Vr19FPqGMK%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;1376&quot; height=&quot;500&quot; data-filename=&quot;image (5).png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;높은 수준에서, Vue 컴포넌트가 마운트될 때 다음과 같은 일이 일어납니다:&lt;/i&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;b&gt;컴파일&lt;/b&gt;: Vue 템플릿은&amp;nbsp;&lt;b&gt;렌더 함수&lt;/b&gt;로 컴파일됩니다. 렌더 함수는 가상 DOM 트리를 반환하는 함수입니다. 이 단계는 빌드 단계에서 미리 수행할 수도 있고, 런타임 컴파일러를 사용해 실시간으로 수행할 수도 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마운트&lt;/b&gt;: 런타임 렌더러가 렌더 함수를 호출하여 반환된 가상 DOM 트리를 순회하고, 이를 기반으로 실제 DOM 노드를 생성합니다. 이 단계는 반응형 효과로 수행되므로, 사용된 모든 반응형 의존성을 추적합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패치&lt;/b&gt;: 마운트 시 사용된 의존성이 변경되면, 효과가 다시 실행됩니다. 이때 새로운, 업데이트된 가상 DOM 트리가 생성됩니다. 런타임 렌더러는 새 트리를 순회하며 이전 트리와 비교하고, 실제 DOM에 필요한 업데이트를 적용합니다.&lt;/li&gt;
&lt;/ol&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;p data-ke-size=&quot;size16&quot;&gt;사용자의 동작에 의해 변수의 값(상태)이 바뀌었을 경우, 이를 화면에 반영해 줄 수 있어야 한다. Vue.js에서는 data() 옵션을 활용해서 변수를 반응형으로 바꿔준다. 해당 변수가 변경되었을 때, 화면에도 반영된다. 한편, methods에 메서드를 선언할 수 있다. 주의할 점은, 화살표 함수는 사용할 수 없다. 일반 함수와 달리 this가 컴포넌트의 인스턴스로 바운드 될 수 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 코드 - data()와 methods&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    // 메서드는 라이프사이클 훅이나 다른 메서드에서 호출할 수 있습니다!
    this.increment()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 코드 - 메서드를 사용하는 template 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;button @click=&quot;increment&quot;&amp;gt;{{ count }}&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;개념&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue.js 어플리케이션은 컴포넌트라는 요소들로 구성되어 있다. 컴포넌트란 UI를 독립적이고 재사용 가능하게 분리한 조각으로, Vue를 활용해서 만드는 앱은 중첩된 컴포넌트의 트리 구조로 이루어져있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (6).png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDLWWI/btsP58sASPW/XuMwghuzDk4zXkxLmBWu80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDLWWI/btsP58sASPW/XuMwghuzDk4zXkxLmBWu80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDLWWI/btsP58sASPW/XuMwghuzDk4zXkxLmBWu80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDLWWI%2FbtsP58sASPW%2FXuMwghuzDk4zXkxLmBWu80%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;1376&quot; height=&quot;542&quot; data-filename=&quot;image (6).png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;542&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;h4 data-ke-size=&quot;size20&quot;&gt;정의&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 .vue 파일로 정의하며, &amp;lt;script&amp;gt;, &amp;lt;template&amp;gt; 블록으로 구성된다. 각각의 .vue 파일을 **싱글 파일 컴포넌트(SFC)**라고 한다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export default {
  data() {
    return {
      count: 0
    }
  }
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;count++&quot;&amp;gt;You clicked me {{ count }} times.&amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로는 .vue 파일 이름과 동일하게 내보내기가 된다. import 해와서 사용할 수 있고, export default 블록 하위에 components로 작성해주어야 한다. 그렇게 하면 &amp;lt;/component&amp;gt;와 같이 부모 컴포넌트에서 넣어서 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
// 이름과 동일하게 내보내기
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;h1&amp;gt;Here is a child component!&amp;lt;/h1&amp;gt;
  &amp;lt;ButtonCounter /&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트 간의 통신&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue.js는 컴포넌트 간의 통신을 지원한다. 부모 컴포넌트에서 자식 컴포넌트로 데이터(값)를 전파하고, 자식 컴포넌트에서 부모 컴포넌트로 이벤트를 전파한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;prop : 부모 컴포넌트 &amp;rarr; 자식 컴포넌트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 컴포넌트에서 전달 받을 변수를 선언하고, 부모 컴포넌트에서 해당 변수에 &lt;b&gt;데이터(값)를 넘겨줄 수 있다.&lt;/b&gt; &lt;b&gt;v-bind(:) 문법&lt;/b&gt;을 활용하여 데이터를 동적으로 전달할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BlogPost.vue(자식 컴포넌트)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export default {
  props: ['title']
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;h4&amp;gt;{{ title }}&amp;lt;/h4&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App.vue(부모 컴포넌트)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export default {
  // ...
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;BlogPost
    v-for=&quot;post in posts&quot;
    :key=&quot;post.id&quot;
    :title=&quot;post.title&quot;
  /&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;emit: 자식 컴포넌트 &amp;rarr; 부모 컴포넌트&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자식에서 부모로 이벤트를 전달한다.&lt;/b&gt; &lt;b&gt;$emit&lt;/b&gt;으로 전파하며, &lt;b&gt;@혹은 v-on&lt;/b&gt;으로 이벤트를 리스닝한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BlogPost.vue(자식 컴포넌트)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export default {
  props: ['title'],
  // emits 명시는 필수 사항은 아니나 권장됨
  emits: ['enlarge-text']
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;blog-post&quot;&amp;gt;
    &amp;lt;h4&amp;gt;{{ title }}&amp;lt;/h4&amp;gt;
    &amp;lt;button @click=&quot;$emit('enlarge-text')&quot;&amp;gt;Enlarge text&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App.vue(부모 컴포넌트)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;...

&amp;lt;template&amp;gt;
  &amp;lt;BlogPost
    v-for=&quot;post in posts&quot;
    :key=&quot;post.id&quot;
    :title=&quot;post.title&quot;
    @enlarge-text=&quot;postFontSize += 0.1&quot;
  /&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동적 컴포넌트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;component&amp;gt;와 :is&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;라는 특별한 속성을 활용해서, 상황에 맞게 동적으로 컴포넌트를 넣을 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;탭 화면&lt;/b&gt;을 구현하는 데 유용하게 활용될 수 있다. is에 전달되는 값은 등록된 컴포넌트의 이름 문자열, 또는 실제 import 한 컴포넌트 객체가 된다.&lt;/p&gt;
&lt;pre class=&quot;vhdl&quot;&gt;&lt;code&gt;&amp;lt;component :is= &quot;currentTab&quot;&amp;gt;&amp;lt;/component&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;b&gt;사용자가 URL을 업데이트하면 해당하는 컨텐츠로 연결&lt;/b&gt;해주는데 이를 &lt;b&gt;라우팅&lt;/b&gt;이라 한다. SPA에서는 &lt;b&gt;클라이언트 사이드 라우팅 방식&lt;/b&gt;을 채택하여, 라우팅이 될 때마다 페이지를 서버에서 다시 로드하지 않고 페이지 내에서 바로 전환해준다. 공식적으로 &lt;b&gt;vue-router 라이브러리&lt;/b&gt;를 지원하며, 크게 보아 라우터 객체를 만들고, 루트 컴포넌트(App.vue)에 등록한 뒤, RouterLink에 해당하는 RouterView를 보여준다. 동적 라우팅, 중첩 라우팅, 프로그래밍 방식 라우팅, 네비게이션 가드 등 다양한 개념들에 대해서도 설명하고 있어, 보다 섬세한 조작이 필요하다면 공식 문서를 참고할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;라우터 객체 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어떤 경로(path)에서 어떤 컴포넌트를 보여줄 지 명시&lt;/b&gt;한다. 참고로 history는 URL과 라우트가 어떻게 매핑되는 지 결정하는 방법을 정의하는 옵션이다. 상세한 설명은 공식 문서의 히스토리 모드 섹션에서 다룬다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { createMemoryHistory, createRouter } from 'vue-router'

import HomeView from './HomeView.vue'
import AboutView from './AboutView.vue'

const routes = [
  { path: '/', component: HomeView },
  { path: '/about', component: AboutView },
]

const router = createRouter({
  history: createMemoryHistory(),
  routes,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;루트 컴포넌트(App.vue)에 등록하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App을 마운트하기 이전에, &lt;b&gt;.use를 활용하여 router를 등록&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;createApp(App)
  .use(router)
  .mount('#app')
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RouterLink-RouterView 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RouterLink를 통해 해당 경로로 이동하는 링크를 만든다. 해당 경로로 이동했을 때, RouterView 에 라우터에 등록해 둔 컴포넌트가 보이게 된다.&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h1&amp;gt;Hello App!&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;
    &amp;lt;strong&amp;gt;현재 라우트 경로:&amp;lt;/strong&amp;gt; {{ $route.fullPath }}
  &amp;lt;/p&amp;gt;
  &amp;lt;nav&amp;gt;
    &amp;lt;RouterLink to=&quot;/&quot;&amp;gt;홈으로 이동&amp;lt;/RouterLink&amp;gt;
    &amp;lt;RouterLink to=&quot;/about&quot;&amp;gt;소개로 이동&amp;lt;/RouterLink&amp;gt;
  &amp;lt;/nav&amp;gt;
  &amp;lt;main&amp;gt;
    &amp;lt;RouterView /&amp;gt;
  &amp;lt;/main&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;참고 자료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;각 항목의 내용은 공식 문서를 참고하여 작성&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SPA&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756009743871&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;Vue.js&quot; data-og-description=&quot;Vue.js - 프로그래시브 자바스트립트 프레임워크&quot; data-og-host=&quot;ko.vuejs.org&quot; data-og-source-url=&quot;https://ko.vuejs.org/guide/extras/ways-of-using-vue.html&quot; data-og-url=&quot;https://ko.vuejs.org/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/VHnrF/hyZDOxX2sl/ExMf0dsRs1sOf8oaBKSLG1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://ko.vuejs.org/guide/extras/ways-of-using-vue.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.vuejs.org/guide/extras/ways-of-using-vue.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/VHnrF/hyZDOxX2sl/ExMf0dsRs1sOf8oaBKSLG1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&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;Vue.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Vue.js - 프로그래시브 자바스트립트 프레임워크&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.vuejs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Vue.js?&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756009745351&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;Vue.js&quot; data-og-description=&quot;Vue.js - 프로그래시브 자바스트립트 프레임워크&quot; data-og-host=&quot;ko.vuejs.org&quot; data-og-source-url=&quot;https://ko.vuejs.org/guide/introduction.html&quot; data-og-url=&quot;https://ko.vuejs.org/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zjo8b/hyZC6TmK71/3BCM06oKBGWnIfZjpiQ2l0/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://ko.vuejs.org/guide/introduction.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.vuejs.org/guide/introduction.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zjo8b/hyZC6TmK71/3BCM06oKBGWnIfZjpiQ2l0/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&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;Vue.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Vue.js - 프로그래시브 자바스트립트 프레임워크&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.vuejs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반응성&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756009746701&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;Vue.js&quot; data-og-description=&quot;Vue.js - 프로그래시브 자바스트립트 프레임워크&quot; data-og-host=&quot;ko.vuejs.org&quot; data-og-source-url=&quot;https://ko.vuejs.org/guide/essentials/reactivity-fundamentals.html&quot; data-og-url=&quot;https://ko.vuejs.org/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ckrTp5/hyZzCsqJzg/rUjYgrrHJXAnb24kkQX431/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://ko.vuejs.org/guide/essentials/reactivity-fundamentals.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.vuejs.org/guide/essentials/reactivity-fundamentals.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ckrTp5/hyZzCsqJzg/rUjYgrrHJXAnb24kkQX431/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&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;Vue.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Vue.js - 프로그래시브 자바스트립트 프레임워크&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.vuejs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컴포넌트&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756009748610&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;Vue.js&quot; data-og-description=&quot;Vue.js - 프로그래시브 자바스트립트 프레임워크&quot; data-og-host=&quot;ko.vuejs.org&quot; data-og-source-url=&quot;https://ko.vuejs.org/guide/essentials/component-basics.html&quot; data-og-url=&quot;https://ko.vuejs.org/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Wc7YH/hyZDclLRIv/MuN87sqEWqbk2x937l4wpk/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://ko.vuejs.org/guide/essentials/component-basics.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.vuejs.org/guide/essentials/component-basics.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Wc7YH/hyZDclLRIv/MuN87sqEWqbk2x937l4wpk/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&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;Vue.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Vue.js - 프로그래시브 자바스트립트 프레임워크&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.vuejs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;라우팅&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756009749851&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;Vue Router | Vue.js를 위한 공식 라우터&quot; data-og-description=&quot;Vue.js 공식 라우터&quot; data-og-host=&quot;router.vuejs.kr&quot; data-og-source-url=&quot;https://router.vuejs.kr/guide/&quot; data-og-url=&quot;https://router.vuejs.kr&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://router.vuejs.kr/guide/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://router.vuejs.kr/guide/&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;Vue Router | Vue.js를 위한 공식 라우터&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Vue.js 공식 라우터&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;router.vuejs.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>웹 개발/Frontend</category>
      <category>frontend</category>
      <category>quickstart</category>
      <category>spa</category>
      <category>Vue</category>
      <category>Vue.js</category>
      <author>devsean</author>
      <guid isPermaLink="true">https://seandailytech.tistory.com/41</guid>
      <comments>https://seandailytech.tistory.com/41#entry41comment</comments>
      <pubDate>Sun, 24 Aug 2025 13:34:36 +0900</pubDate>
    </item>
    <item>
      <title>Prometheus 훑어보기</title>
      <link>https://seandailytech.tistory.com/40</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;메트릭 수집의 필요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 수가 늘어나고, 서비스가 고도화됨에 따라 &lt;b&gt;서버의 구성 요소(컴포넌트)들이 다분화&lt;/b&gt;된다. 원활한 서비스 운영을 위해 각 &lt;b&gt;컴포넌트들의 상태를 쉽게 파악&lt;/b&gt;할 수 있어야 한다. 이를 위해 &lt;b&gt;메트릭을 설정&lt;/b&gt;하고 운영이 잘 되고 있는지, 이슈는 없는 지를 체크할 수 있다. 프로메테우스를 활용하여, &lt;b&gt;시계열로 메트릭의 상태를 추적 관찰&lt;/b&gt;할 수 있다. 참고로 유용하게 활용되는 메트릭은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호스트 단위 메트릭 : CPU, 메모리, 디스크 I/O 등&lt;/li&gt;
&lt;li&gt;종합(aggregated) 메트릭 : DB 계층의 성능, 캐시 계층의 성능 등&lt;/li&gt;
&lt;li&gt;핵심 비즈니스 메트릭 : DAU(일별 능동 사용자), Revenue, Retention(재방문) 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;523&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I8keb/btsPLdCVdXt/zMapuvIX55Kqy9L8UIi1D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I8keb/btsPLdCVdXt/zMapuvIX55Kqy9L8UIi1D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I8keb/btsPLdCVdXt/zMapuvIX55Kqy9L8UIi1D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI8keb%2FbtsPLdCVdXt%2FzMapuvIX55Kqy9L8UIi1D0%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;497&quot; height=&quot;523&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;497&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Prometheus?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로메테우스는 CNCF에 소속된 오픈소스 프로젝트이며, 시계열 데이터를 처리하는 것이 주 목적인 &lt;b&gt;Time-Series DB&lt;/b&gt;이다. 주로 메트릭 데이터에 대한 &lt;b&gt;APM(Application Performance Monitoring) 구축&lt;/b&gt;을 목적으로 하므로, &lt;b&gt;컨테이너 기반의 분산 서비스(MSA 등)에서 활용&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정된 수집 주기에 맞춰 수집 대상 요소들로부터 메트릭들을 수집해오며, &lt;b&gt;PromQL&lt;/b&gt;이라는 쿼리 언어를 활용하여 조회할 수 있다. 메트릭을 수집하는 &lt;b&gt;Pull 방식&lt;/b&gt;을 채택하여 모니터링 설정을 Prometheus에서 편하게 관리할 수 있다. &lt;b&gt;쿠버네티스 환경&lt;/b&gt;에서 적용하기 위해, &lt;b&gt;Prometheus Operator&lt;/b&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;Prometheus 데이터 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고하여 학습했던 블로그의 설명이 깔끔해서 예시를 그대로 가져왔다. 쿼리했을 때 화면에 보이는 결과인 &lt;b&gt;기본적인 데이터 구조&lt;/b&gt;와, &lt;b&gt;시계열&lt;/b&gt; &lt;b&gt;관점에서 볼 수 있는 데이터 구조&lt;/b&gt;가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본적인 데이터 구조&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image2.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbQ1q1/btsPMOhQuQS/TktpfJB2kmxxoNQsKpzG80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbQ1q1/btsPMOhQuQS/TktpfJB2kmxxoNQsKpzG80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbQ1q1/btsPMOhQuQS/TktpfJB2kmxxoNQsKpzG80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbQ1q1%2FbtsPMOhQuQS%2FTktpfJB2kmxxoNQsKpzG80%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;743&quot; height=&quot;178&quot; data-filename=&quot;image2.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터 이름 : 지표(Metrics)를 구분하기 위한 고유한 이름으로, 이 이름만 검색해도 해당하는 데이터의 목록을 조회할 수 있다.&lt;/li&gt;
&lt;li&gt;라벨 (Label) : 동일한 지표에 해당하는 데이터 각각을 구분하기 위한 식별자이다.&lt;/li&gt;
&lt;li&gt;데이터 값 (Scalar) : 데이터의 값을 나타낸다. 프로메테우스는 float64 범위의 실수만을 데이터 값으로 갖는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시계열 관점에서의 데이터 구조(Vector)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus는 시계열 DB이기 때문에, 각 Metrics에 해당하는 &lt;b&gt;데이터들은 시간(Timestamp)과 함께 저장&lt;/b&gt;된다. 아래와 같이 쿼리 뒤에 &lt;b&gt;대괄호([])&lt;/b&gt;를 열고 시간 값을 입력하면, &lt;b&gt;Value에 해당하는 Timestamp와 함께 데이터가 조회&lt;/b&gt;된다. 다음은 최근 1분[1m]에 해당하는 데이터를 가져오는 예시이다. 수집 주기인 10초 단위로, Value에 값과 Timestamp를 함께 출력한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image3.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnJmxf/btsPLeuVAMt/1OSi7OyMA8jjrX0eKx9xo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnJmxf/btsPLeuVAMt/1OSi7OyMA8jjrX0eKx9xo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnJmxf/btsPLeuVAMt/1OSi7OyMA8jjrX0eKx9xo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnJmxf%2FbtsPLeuVAMt%2F1OSi7OyMA8jjrX0eKx9xo0%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;743&quot; height=&quot;310&quot; data-filename=&quot;image3.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 개념과 함께, 데이터들은 &lt;b&gt;&lt;span data-token-index=&quot;1&quot;&gt;Vector&lt;/span&gt;&lt;/b&gt;라는 단위로 분류할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image4.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czkzWi/btsPL0XiqkU/JZiylVkZKKKBVfMNauNDhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czkzWi/btsPL0XiqkU/JZiylVkZKKKBVfMNauNDhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czkzWi/btsPL0XiqkU/JZiylVkZKKKBVfMNauNDhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczkzWi%2FbtsPL0XiqkU%2FJZiylVkZKKKBVfMNauNDhK%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;743&quot; height=&quot;348&quot; data-filename=&quot;image4.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;348&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Sample : 데이터 이름, 라벨, 스칼라 한 건을 의미한다.&lt;/li&gt;
&lt;li&gt;Instant Vector : &lt;b&gt;동일한 시간대&lt;/b&gt;에 속하는, 그러나 &lt;b&gt;다른 라벨을 가진 데이터(Sample)의 집합&lt;/b&gt;을 의미한다. 데이터 이름으로만 조회했을 때 불러와지는 데이터 목록은, 가장 최신 시점에 해당하는 Instant Vector를 반환한다.&lt;/li&gt;
&lt;li&gt;Range Vector : 앞선 예시의 alice_k106_http_requests[1m]과 같이 &lt;b&gt;특정 시간대의 데이터&lt;/b&gt;를 가져올 때 조회되는 &lt;b&gt;데이터(Sample)의 집합&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PromQL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로메테우스는 데이터를 쿼리하기 위해, PromQL이라는 자체 쿼리 언어를 사용한다. 쿼리 문법은 Prometheus 공식 문서에서 확인할 수 있으며, 참고한 블로그에 소개된 자주 사용되는 문법들 위주로 정리해두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본 문법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Label Selector&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Metrics의 이름 뒤에 &lt;b&gt;{}을 통해&lt;/b&gt; Label 셀렉터를 추가하여, 셀렉터의 &lt;b&gt;조건에 맞는 데이터를 반환&lt;/b&gt;한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image5.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PhRIt/btsPLHKnCot/tc8WavXvcTvc8cQruKeVi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PhRIt/btsPLHKnCot/tc8WavXvcTvc8cQruKeVi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PhRIt/btsPLHKnCot/tc8WavXvcTvc8cQruKeVi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPhRIt%2FbtsPLHKnCot%2Ftc8WavXvcTvc8cQruKeVi1%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;743&quot; height=&quot;120&quot; data-filename=&quot;image5.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특정 시간 이전부터 현재까지의 데이터 반환&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대괄호[]&lt;/b&gt;를 사용하여, 현재를 기준으로 특정 시간 만큼의 데이터를 가져온다. 사용할 수 있는 시간 단위는 s, m, h, d, w, y 6가지이며 반환되는 데이터는 &lt;b&gt;Range Vector 타입&lt;/b&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image6.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmhDzH/btsPMXFKlIo/c1dYqAp2LMLVr0ydG0ctz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmhDzH/btsPMXFKlIo/c1dYqAp2LMLVr0ydG0ctz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmhDzH/btsPMXFKlIo/c1dYqAp2LMLVr0ydG0ctz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmhDzH%2FbtsPMXFKlIo%2Fc1dYqAp2LMLVr0ydG0ctz0%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;743&quot; height=&quot;133&quot; data-filename=&quot;image6.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;133&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정규 표현식을 사용한 Label 쿼리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정규 표현식&lt;/b&gt;을 사용해 특정 라벨의 값을 가지는 데이터를 반환할 수 있다. 이 때, 정규 표현식은&amp;nbsp;RE2 문법을 따른다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image7.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coh03C/btsPNfTEuK0/ZjiWSKBbZaYtYUJW1n1xPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coh03C/btsPNfTEuK0/ZjiWSKBbZaYtYUJW1n1xPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coh03C/btsPNfTEuK0/ZjiWSKBbZaYtYUJW1n1xPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcoh03C%2FbtsPNfTEuK0%2FZjiWSKBbZaYtYUJW1n1xPk%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;743&quot; height=&quot;130&quot; data-filename=&quot;image7.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 연산자를 통한 데이터 쿼리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다른 데이터 간에 같은 라벨을 가지는 데이터&lt;/b&gt;가 존재한다면, &lt;b&gt;+, - 등의 연산&lt;/b&gt;이 가능하다. 예를 들어 아래와 같이 두 개의 라벨이 일치한다면, 직접 데이터를 빼는 것이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image8.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QBwyk/btsPK22BQjW/Ma91SPFcnNDDLWX0PfigvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QBwyk/btsPK22BQjW/Ma91SPFcnNDDLWX0PfigvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QBwyk/btsPK22BQjW/Ma91SPFcnNDDLWX0PfigvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQBwyk%2FbtsPK22BQjW%2FMa91SPFcnNDDLWX0PfigvK%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;743&quot; height=&quot;109&quot; data-filename=&quot;image8.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;109&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image9.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T7pKY/btsPMVun7H0/rCoAoXpCMMOMVkTkYANp8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T7pKY/btsPMVun7H0/rCoAoXpCMMOMVkTkYANp8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T7pKY/btsPMVun7H0/rCoAoXpCMMOMVkTkYANp8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT7pKY%2FbtsPMVun7H0%2FrCoAoXpCMMOMVkTkYANp8K%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;743&quot; height=&quot;144&quot; data-filename=&quot;image9.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image10.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKMOZJ/btsPMYxSaEG/54p7lzwCXGgCALk7Ln3jaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKMOZJ/btsPMYxSaEG/54p7lzwCXGgCALk7Ln3jaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKMOZJ/btsPMYxSaEG/54p7lzwCXGgCALk7Ln3jaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKMOZJ%2FbtsPMYxSaEG%2F54p7lzwCXGgCALk7Ln3jaK%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;743&quot; height=&quot;135&quot; data-filename=&quot;image10.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;135&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Recording Rules&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 제공되는 Metrics 이외에도, &lt;b&gt;rule을 정의하여 데이터를 재정의&lt;/b&gt;할 수 있다. 자주 사용되는 쿼리를 미리 정의해둔 뒤, 주기적으로 rule에 정의된 쿼리를 수행해 그 결과물을 새로운 데이터 Metrics로서 다시 저장한다. 이렇게 함으로써 &lt;b&gt;자주 사용하는 쿼리에 의미를 부여&lt;/b&gt;할 수도 있고, Grafana와 같은 &lt;b&gt;Dashboard를 활용할 때에도 매 번 새로운 쿼리를 실행하는 부담을 줄여줄 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sum, avg, rate 등의 기본 함수&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sum은 합계를, avg는 평균&lt;/b&gt;을 계산한다. by(label1, label2, &amp;hellip;)와 같이 특정 label들을 기준으로 계산할 수도 있다. &lt;b&gt;rate&lt;/b&gt;는 기간 내에서의 &lt;b&gt;1초 당 증감률&lt;/b&gt;을 계산한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image11.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;233&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdcinN/btsPNH3qTqq/j72wm3FgSSqkMwJvKY6LEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdcinN/btsPNH3qTqq/j72wm3FgSSqkMwJvKY6LEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdcinN/btsPNH3qTqq/j72wm3FgSSqkMwJvKY6LEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdcinN%2FbtsPNH3qTqq%2Fj72wm3FgSSqkMwJvKY6LEK%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;743&quot; height=&quot;233&quot; data-filename=&quot;image11.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;233&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image12.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lAKuM/btsPN3k3iNv/GYstYLWoEOQTxZM6gmoMck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lAKuM/btsPN3k3iNv/GYstYLWoEOQTxZM6gmoMck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lAKuM/btsPN3k3iNv/GYstYLWoEOQTxZM6gmoMck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlAKuM%2FbtsPN3k3iNv%2FGYstYLWoEOQTxZM6gmoMck%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;743&quot; height=&quot;234&quot; data-filename=&quot;image12.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image13.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4PQLs/btsPKVvNtWE/dvV0AkjvXZkrGL2ptuJvQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4PQLs/btsPKVvNtWE/dvV0AkjvXZkrGL2ptuJvQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4PQLs/btsPKVvNtWE/dvV0AkjvXZkrGL2ptuJvQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4PQLs%2FbtsPKVvNtWE%2FdvV0AkjvXZkrGL2ptuJvQK%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;743&quot; height=&quot;250&quot; data-filename=&quot;image13.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image14.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nBh36/btsPN3FjSNM/XQSKvFnkHqcyvkPUKzU1Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nBh36/btsPN3FjSNM/XQSKvFnkHqcyvkPUKzU1Hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nBh36/btsPN3FjSNM/XQSKvFnkHqcyvkPUKzU1Hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnBh36%2FbtsPN3FjSNM%2FXQSKvFnkHqcyvkPUKzU1Hk%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;743&quot; height=&quot;250&quot; data-filename=&quot;image14.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image15.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3jJ8D/btsPMoqbdIh/kDu7ATddurkEdn7xVUb3Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3jJ8D/btsPMoqbdIh/kDu7ATddurkEdn7xVUb3Pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3jJ8D/btsPMoqbdIh/kDu7ATddurkEdn7xVUb3Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3jJ8D%2FbtsPMoqbdIh%2FkDu7ATddurkEdn7xVUb3Pk%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;743&quot; height=&quot;232&quot; data-filename=&quot;image15.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;232&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;h4 data-ke-size=&quot;size20&quot;&gt;Vector Matching&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus는 Vector Matching이라는 기능을 통해, &lt;b&gt;Vector 간 연산을 제공&lt;/b&gt;한다. Vector 간 연산은 기본적으로는 &lt;b&gt;라벨이 일치하는 두 Vector&lt;/b&gt; 간에 이루어지는데, &lt;b&gt;라벨이 달라도 on과 ignoring 키워드를 활용해서 연산을 가능&lt;/b&gt;케 한다. &lt;b&gt;ignoring&lt;/b&gt;은 &lt;b&gt;명시된 라벨들을 무시&lt;/b&gt;하고 연산을 수행하며, &lt;b&gt;on&lt;/b&gt;은 &lt;b&gt;명시된 라벨을 기준으로만&lt;/b&gt; 연산을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한편 연산은 &lt;b&gt;One-to-One, One-to-Many, Many-to-One&lt;/b&gt; 3가지를 제공한다. 쉽게 이야기해서 Cardinality 기준으로 &lt;b&gt;Many인 벡터가 왼쪽이면 group_left(), 오른쪽이면 group_right()를 사용&lt;/b&gt;하면 된다. &lt;b&gt;괄호 안에 라벨을 넣어서, One인 벡터 쪽의 라벨이 보존&lt;/b&gt;되도록 할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;예시 : One-to-One&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image16.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;105&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IGwVJ/btsPOlsh6K1/Pn1aLVfKFmQlkIoI13QNx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IGwVJ/btsPOlsh6K1/Pn1aLVfKFmQlkIoI13QNx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IGwVJ/btsPOlsh6K1/Pn1aLVfKFmQlkIoI13QNx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIGwVJ%2FbtsPOlsh6K1%2FPn1aLVfKFmQlkIoI13QNx1%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;743&quot; height=&quot;105&quot; data-filename=&quot;image16.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;105&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image17.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJS0m/btsPMWGON3M/Nn7huBkEQTMr3vfNFn7GVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJS0m/btsPMWGON3M/Nn7huBkEQTMr3vfNFn7GVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJS0m/btsPMWGON3M/Nn7huBkEQTMr3vfNFn7GVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJS0m%2FbtsPMWGON3M%2FNn7huBkEQTMr3vfNFn7GVK%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;743&quot; height=&quot;371&quot; data-filename=&quot;image17.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;예시 : One-to-Many, Many-to-One&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image18.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q9H0K/btsPN96BR12/xeRbYKMaA2yuvLZrE6bxgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q9H0K/btsPN96BR12/xeRbYKMaA2yuvLZrE6bxgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q9H0K/btsPN96BR12/xeRbYKMaA2yuvLZrE6bxgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq9H0K%2FbtsPN96BR12%2FxeRbYKMaA2yuvLZrE6bxgk%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;743&quot; height=&quot;103&quot; data-filename=&quot;image18.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;103&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image19.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/85d0x/btsPObpPvMj/KUjSuCMn7SMKv13Awtn1d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/85d0x/btsPObpPvMj/KUjSuCMn7SMKv13Awtn1d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/85d0x/btsPObpPvMj/KUjSuCMn7SMKv13Awtn1d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F85d0x%2FbtsPObpPvMj%2FKUjSuCMn7SMKv13Awtn1d0%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;743&quot; height=&quot;378&quot; data-filename=&quot;image19.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;예시 : on&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image20.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctkylr/btsPLE7ZDHv/imFLXKjWBjhV5FcSsB4It0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctkylr/btsPLE7ZDHv/imFLXKjWBjhV5FcSsB4It0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctkylr/btsPLE7ZDHv/imFLXKjWBjhV5FcSsB4It0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fctkylr%2FbtsPLE7ZDHv%2FimFLXKjWBjhV5FcSsB4It0%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;743&quot; height=&quot;94&quot; data-filename=&quot;image20.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image21.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCA1gx/btsPLlHBSZf/fKbizwfRkanHgYz497CLUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCA1gx/btsPLlHBSZf/fKbizwfRkanHgYz497CLUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCA1gx/btsPLlHBSZf/fKbizwfRkanHgYz497CLUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCA1gx%2FbtsPLlHBSZf%2FfKbizwfRkanHgYz497CLUK%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;743&quot; height=&quot;109&quot; data-filename=&quot;image21.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;109&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image22.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AsTd7/btsPNkglhev/XZcBYhahLZxkzEzodMhLrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AsTd7/btsPNkglhev/XZcBYhahLZxkzEzodMhLrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AsTd7/btsPNkglhev/XZcBYhahLZxkzEzodMhLrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAsTd7%2FbtsPNkglhev%2FXZcBYhahLZxkzEzodMhLrK%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;743&quot; height=&quot;153&quot; data-filename=&quot;image22.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;예시 : group_left(success_message)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상기 예시 : on과 비교하여, group_left 뒤에 success_message 추가&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image23.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tZ5Eq/btsPLWtPPLE/bf1Bdsp7lmfHHgqBNziyG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tZ5Eq/btsPLWtPPLE/bf1Bdsp7lmfHHgqBNziyG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tZ5Eq/btsPLWtPPLE/bf1Bdsp7lmfHHgqBNziyG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtZ5Eq%2FbtsPLWtPPLE%2Fbf1Bdsp7lmfHHgqBNziyG1%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;743&quot; height=&quot;357&quot; data-filename=&quot;image23.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 1 - 1장(알렉스 쉬, 2021)&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1754650002417&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;169. [Prometheus] 1편 : Prometheus (프로메테우스) 사용 방법, 기본 개념, 데이터 구조&quot; data-og-description=&quot;이번 포스트에서는 프로메테우스 (Prometheus) 에 대한 전반적인 사용 방법에 대해서 다룬다. k8s와 같은 ...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://blog.naver.com/alice_k106/221535163599&quot; data-og-url=&quot;https://blog.naver.com/alice_k106/221535163599&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Pm2W4/hyZvpMhfue/AL83jbAFs4wnYrkthPox9k/img.png?width=743&amp;amp;height=422&amp;amp;face=0_0_743_422&quot;&gt;&lt;a href=&quot;https://blog.naver.com/alice_k106/221535163599&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.naver.com/alice_k106/221535163599&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Pm2W4/hyZvpMhfue/AL83jbAFs4wnYrkthPox9k/img.png?width=743&amp;amp;height=422&amp;amp;face=0_0_743_422');&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;169. [Prometheus] 1편 : Prometheus (프로메테우스) 사용 방법, 기본 개념, 데이터 구조&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스트에서는 프로메테우스 (Prometheus) 에 대한 전반적인 사용 방법에 대해서 다룬다. k8s와 같은 ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1754650036314&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;170. [Prometheus] 2편. PromQL 사용하기, k8s에서 Meta Label과 relabel을 활용한 기본 라벨 설정, 그 외의 이&quot; data-og-description=&quot;이번 포스트에서는 프로메테우스에서 PromQL을 사용하는 방법과 기본 라벨 설정 방법에 대해서 다룬다.&amp;amp;...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://blog.naver.com/alice_k106/221535575875&quot; data-og-url=&quot;https://blog.naver.com/alice_k106/221535575875&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HwGfg/hyZuIrV2LK/diexLgK3Y9A1vRZLFyg7s1/img.png?width=743&amp;amp;height=414&amp;amp;face=0_0_743_414&quot;&gt;&lt;a href=&quot;https://blog.naver.com/alice_k106/221535575875&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.naver.com/alice_k106/221535575875&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HwGfg/hyZuIrV2LK/diexLgK3Y9A1vRZLFyg7s1/img.png?width=743&amp;amp;height=414&amp;amp;face=0_0_743_414');&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;170. [Prometheus] 2편. PromQL 사용하기, k8s에서 Meta Label과 relabel을 활용한 기본 라벨 설정, 그 외의 이&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스트에서는 프로메테우스에서 PromQL을 사용하는 방법과 기본 라벨 설정 방법에 대해서 다룬다.&amp;amp;...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1754650048498&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;Querying basics | Prometheus&quot; data-og-description=&quot;Prometheus project documentation for Querying basics&quot; data-og-host=&quot;prometheus.io&quot; data-og-source-url=&quot;https://prometheus.io/docs/prometheus/latest/querying/basics/&quot; data-og-url=&quot;https://prometheus.io/docs/prometheus/latest/querying/basics/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mCFey/hyZuHfwpn4/qnYKIknG5yVBuo6rzXh0EK/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/querying/basics/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prometheus.io/docs/prometheus/latest/querying/basics/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mCFey/hyZuHfwpn4/qnYKIknG5yVBuo6rzXh0EK/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&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;Querying basics | Prometheus&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Prometheus project documentation for Querying basics&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prometheus.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1754650056179&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;Defining recording rules | Prometheus&quot; data-og-description=&quot;Prometheus project documentation for Defining recording rules&quot; data-og-host=&quot;prometheus.io&quot; data-og-source-url=&quot;https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/&quot; data-og-url=&quot;https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tNmyR/hyZuH7DW4H/9HSagaNNkXLDFAIrC7jBsK/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tNmyR/hyZuH7DW4H/9HSagaNNkXLDFAIrC7jBsK/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&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;Defining recording rules | Prometheus&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Prometheus project documentation for Defining recording rules&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prometheus.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1754650083824&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;Prometheus Operator를 사용해 Kubernetes 환경에서 Prometheus  구성하기&quot; data-og-description=&quot;Prometheus는 CNCF의 몇 안되는 graduate 프로젝트 중 하나인 시계열 모니터링 시스템입니다. 이런 Prometheus는 일반적인 VM 환경에서도 사용할 수 있지만 Kuberenetes에서 사용하는 MSA 구성에도 사용할 수 &quot; data-og-host=&quot;nangman14.tistory.com&quot; data-og-source-url=&quot;https://nangman14.tistory.com/75#1.%20Prometheus%20Operator%ED%99%98%EA%B2%BD%20%EC%A4%80%EB%B9%84-1&quot; data-og-url=&quot;https://nangman14.tistory.com/75&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Y7uDq/hyZvnASNZI/KTVtF941wztkUeGWsnj9r0/img.png?width=280&amp;amp;height=280&amp;amp;face=0_0_280_280,https://scrap.kakaocdn.net/dn/bk9xDd/hyZvmaU1mZ/HPNnpTm6WTaekYuhUhxwOK/img.png?width=280&amp;amp;height=280&amp;amp;face=0_0_280_280,https://scrap.kakaocdn.net/dn/eDvaiz/hyZvkxoZAJ/9QFpNStODI1QEglzgApFz0/img.png?width=2880&amp;amp;height=966&amp;amp;face=0_0_2880_966&quot;&gt;&lt;a href=&quot;https://nangman14.tistory.com/75#1.%20Prometheus%20Operator%ED%99%98%EA%B2%BD%20%EC%A4%80%EB%B9%84-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nangman14.tistory.com/75#1.%20Prometheus%20Operator%ED%99%98%EA%B2%BD%20%EC%A4%80%EB%B9%84-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Y7uDq/hyZvnASNZI/KTVtF941wztkUeGWsnj9r0/img.png?width=280&amp;amp;height=280&amp;amp;face=0_0_280_280,https://scrap.kakaocdn.net/dn/bk9xDd/hyZvmaU1mZ/HPNnpTm6WTaekYuhUhxwOK/img.png?width=280&amp;amp;height=280&amp;amp;face=0_0_280_280,https://scrap.kakaocdn.net/dn/eDvaiz/hyZvkxoZAJ/9QFpNStODI1QEglzgApFz0/img.png?width=2880&amp;amp;height=966&amp;amp;face=0_0_2880_966');&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;Prometheus Operator를 사용해 Kubernetes 환경에서 Prometheus 구성하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Prometheus는 CNCF의 몇 안되는 graduate 프로젝트 중 하나인 시계열 모니터링 시스템입니다. 이런 Prometheus는 일반적인 VM 환경에서도 사용할 수 있지만 Kuberenetes에서 사용하는 MSA 구성에도 사용할 수&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nangman14.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;향후 참고할 자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*참고했던 블로그 저자가 출판한 책으로 2025년 7월 출간되었다. 시간될 때 읽어볼 예정이다.&lt;/p&gt;
&lt;figure id=&quot;og_1754649900959&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;도커/쿠버네티스 | 용찬호 - 교보문고&quot; data-og-description=&quot;도커/쿠버네티스 | 쿠버네티스와 도커의 기본 사용 방법을 정확히 이해하는 것을 목표로 합니다!도커 컨테이너는 애플리케이션을 배포하기 위한 새로운 패러다임을 제시하는 가상화 패러다임&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000216911094&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000216911094&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iMjmz/hyZuwLOqQm/I2R1K5VfesZ5smxZ3dXYyK/img.jpg?width=458&amp;amp;height=585&amp;amp;face=0_0_458_585,https://scrap.kakaocdn.net/dn/bJSVxd/hyZuD5faYD/k5jdlzBHJcUmk33fZkkifK/img.jpg?width=458&amp;amp;height=585&amp;amp;face=0_0_458_585,https://scrap.kakaocdn.net/dn/qsyyh/hyZuCZxeR8/Iavx4ATUy8ls0cFWhIPY3K/img.png?width=335&amp;amp;height=335&amp;amp;face=0_0_335_335&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000216911094&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000216911094&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iMjmz/hyZuwLOqQm/I2R1K5VfesZ5smxZ3dXYyK/img.jpg?width=458&amp;amp;height=585&amp;amp;face=0_0_458_585,https://scrap.kakaocdn.net/dn/bJSVxd/hyZuD5faYD/k5jdlzBHJcUmk33fZkkifK/img.jpg?width=458&amp;amp;height=585&amp;amp;face=0_0_458_585,https://scrap.kakaocdn.net/dn/qsyyh/hyZuCZxeR8/Iavx4ATUy8ls0cFWhIPY3K/img.png?width=335&amp;amp;height=335&amp;amp;face=0_0_335_335');&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;도커/쿠버네티스 | 용찬호 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;도커/쿠버네티스 | 쿠버네티스와 도커의 기본 사용 방법을 정확히 이해하는 것을 목표로 합니다!도커 컨테이너는 애플리케이션을 배포하기 위한 새로운 패러다임을 제시하는 가상화 패러다임&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps/Monitoring</category>
      <category>APM</category>
      <category>K8S</category>
      <category>Kubernetes</category>
      <category>Montoring</category>
      <category>Prometheus</category>
      <category>PromQL</category>
      <author>devsean</author>
      <guid isPermaLink="true">https://seandailytech.tistory.com/40</guid>
      <comments>https://seandailytech.tistory.com/40#entry40comment</comments>
      <pubDate>Fri, 8 Aug 2025 19:51:06 +0900</pubDate>
    </item>
    <item>
      <title>Javascript의 비동기 처리(콜백 함수, 프로미스, async/await)</title>
      <link>https://seandailytech.tistory.com/39</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기(Asynchronous)와 자바스크립트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;비동기 처리란 &lt;b&gt;특정 코드의 연산이 끝날 때까지 실행을 기다리지 않고 다음 코드를 먼저 실행&lt;/b&gt;&lt;/span&gt;하는 방법으로, Javascript에서는 주로 네트워크 요청(Ajax 요청), 파일 읽기/쓰기, 타이머 함수(setTimeout,setInterval), 이벤트 처리 등의 상황에서 사용된다. 만약 이러한 작업들이 &lt;b&gt;&lt;span data-token-index=&quot;2&quot;&gt;동기식으로 처리된다면&lt;/span&gt;&lt;/b&gt; 다른 코드들의 실행이 블로킹되고 대기 시간이 길어지므로 &lt;b&gt;&lt;span data-token-index=&quot;4&quot;&gt;웹 사이트의 성능과 사용자에게 부정적인 영향&lt;/span&gt;을 줄 수 있다.&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;비동기 처리를 위해 Javascript에서는 콜백 함수를 제공한다. 매개변수로 함수를 전달받아 &lt;b&gt;내부에서 실행&lt;/b&gt;하는 함수이다. &lt;b&gt;필요한 시점&lt;/b&gt;에 매개변수로 받은 함수를 &lt;b&gt;실행&lt;/b&gt;하여 자바스크립트의 &lt;b&gt;비동기적인 작업을 처리&lt;/b&gt;하는 데 사용한다. 콜백 함수를 활용하면 비동기적인 처리를 &lt;b&gt;순서대로 처리할 수 있도록&lt;/b&gt;(동기적으로) &lt;b&gt;바꿀 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function timecheck(callback) {
    setTimeout(() =&amp;gt; {
        console.log(&quot;1번 호출&quot;);
        callback();
    }, 3000);
}

function printview() {
    console.log(&quot;2번 호출&quot;);
}

timecheck(printview);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콜백 지옥&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 함수를 순차적으로 실행하기 위해 콜백 함수만 사용한다면, 소위 얘기하는 &lt;b&gt;콜백 지옥&lt;/b&gt;에 빠질 수 있다. 피자를 주문하는 과정을 생각해보자. 토핑을 고르고, 주문을 하고, 주문을 받고, 피자를 먹는다. 각 단계를 콜백 함수를 이용해 순서가 어긋나지 않도록 처리했다. 코드를 읽기도 힘들고, 콜백을 계속해서 작성해줘야 해서 불편하다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;chooseToppings(function (toppings) {
  placeOrder(
    toppings,
    function (order) {
      collectOrder(
        order,
        function (pizza) {
          eatPizza(pizza);
        },
        failureCallback,
      );
    },
    failureCallback,
  );
}, failureCallback);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해, Promise를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로미스(Promise)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise는 &lt;b&gt;미래에 어떤 종류의 결과가 반환됨을 약속(promise)&lt;/b&gt;해주는 객체이다. 콜백 함수를 사용할 때보다 유연하고 편리하게 비동기 처리를 할 수 있다. &lt;b&gt;Promise는&lt;/b&gt; &lt;b&gt;생성과 동시에 비동기 작업을 실행&lt;/b&gt;하고, 그 &lt;b&gt;결과를 Promise 객체에 반환&lt;/b&gt;한다. 방금&amp;nbsp;확인한 예시에 Promise 방식을 적용하면, 다음과 같이 간결하게 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// Promise + 화살표 함수
chooseToppings()
  .then((toppings) =&amp;gt; placeOrder(toppings))
  .then((order) =&amp;gt; collectOrder(order))
  .then((pizza) =&amp;gt; eatPizza(pizza))
  .catch(failureCallback);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대기(pending)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로미스를 &lt;b&gt;호출&lt;/b&gt;하면 프로미스 객체가 &lt;b&gt;대기 상태&lt;/b&gt;가 되고, 프로미스에서 자체적으로 제공하는 r&lt;b&gt;esolve, reject라는 콜백 함수를 인자로 받아 상황에 맞게 작업 처리&lt;/b&gt;를 할 수 있다. 대기 상태 이후에 비동기 작업이 성공하면(이행) resolve(&amp;rdquo;성공!&amp;rdquo;)와 같이 결과(&amp;rdquo;성공!&amp;rdquo;)를 전달하고, 실패하면 reject(&amp;rdquo;실패!&amp;rdquo;)와 같이 오류 객체(Error(&amp;rdquo;실패&amp;rdquo;))를 전달한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;let promise = new Promise((resolve, reject) =&amp;gt; {
  const success = true;
  if (success) {
    resolve(&quot;성공!&quot;);
  } else {
    reject(&quot;실패!&quot;); // Error 객체 반환, 아래와 같이 Error 던져도 동일하게 동작
    // throw new Error(&quot;실패&quot;);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이행(fulfill)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 작업이 &lt;b&gt;성공적으로 완료&lt;/b&gt;되어 결과값을 가지고 있는 상태이다. 이 때 &lt;b&gt;resolve 콜백이 실행&lt;/b&gt;되고, 프로미스 객체는 이행 상태가 된다. &lt;b&gt;then() 함수&lt;/b&gt;를 이용하여 resolve의 결과값을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한편, then에서는 인자를 2개 받을 수도 있다. 첫 번째 인자는 resolve 콜백 함수의 결과값을, reject 콜백 함수의 오류 객체를 받는다. 그래서 성공 상태일때는 resolve의 결과값을, 실패 상태일때는 reject의 결과값을 받도록 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// .then의 인자로 2개의 함수를 받는 예시
// 일반적으로는 .then에서 성공했을 때의 처리를 하고 .catch로 실패했을 때의 처리를 한다.
promise.then(
  result =&amp;gt; {
    console.log(&quot;성공 콜백:&quot;, result);
  },
  error =&amp;gt; {
    console.log(&quot;실패 콜백:&quot;, error);
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실패(reject)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 작업이 &lt;b&gt;실패하여 오류나 예외가 발생&lt;/b&gt;한 상태이다. 프로미스의 콜백 함수로 전달받은 매개변수인 &lt;b&gt;reject 콜백이 실행&lt;/b&gt;되고 프로미스 객체는 실패 상태가 된다. &lt;b&gt;catch() 함수&lt;/b&gt;를 이용하여 결과값을 받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 위의 예제를 다음과 같이 작성할 수 있음(일반적으로 사용하는 방법)
promise.then(
  result =&amp;gt; {
    console.log(&quot;성공 콜백:&quot;, result);
  })
.catch(error =&amp;gt; {
    console.log(&quot;실패 콜백:&quot;, error);
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;async와 await&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로미스로 가독성과 편리함을 높이기는 했지만, 이 또한 .then()을 계속해줘서 붙이기 때문에 프로미스 처리가 계속된다면 프로미스 지옥에 빠질 수도 있다. async/await 문법을 활용하여 더 편리하게 비동기 처리를 할 수 있다.&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;async/await 문법은 함수를 선언할 때 앞부분에 async 키워드를 작성하여 비동기 함수로 만든다. a&lt;b&gt;sync가 앞에 붙은 함수는 프로미스를 반환&lt;/b&gt;한다. &lt;b&gt;await는 async 함수 안에서만 동작&lt;/b&gt;하고 마치 &lt;b&gt;동기식처럼&lt;/b&gt; 프로미스 처리가 끝날 때까지 &lt;b&gt;기다린다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Promise를 작성하는 헬퍼 함수 작성
function helperPromise() {
	return new Promise((resolve, reject) =&amp;gt; {
	  const success = true;
	  if (success) {
	    resolve(&quot;성공!&quot;);
	  } else {
	    reject(&quot;실패!&quot;);
	    // throw new Error(&quot;실패&quot;);
		  }
		});
}

// await는 단독으로 쓰일 수 없으므로, async로 감싼다.
async function useHelper() {
	result = await helperPromise();
	console.log(&quot;성공 콜백:&quot;, result);
}

useHelper();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;async 예외 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async 함수 내부의 프로미스에서 예외가 되면(거부되면) throw로 에러를 반환한다. try/catch를 사용하면 프로미스가 거부될 때 해당 예외에 대한 추가 로직이나 오류 메세지를 출력할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;async function useHelper() {
	try{
		result = await helperPromise();
		console.log(&quot;성공 콜백:&quot;, result);
	} catch (e) {
        console.log(&quot;실패 콜백:&quot;, error);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고 자료&lt;/h3&gt;
&lt;figure id=&quot;og_1753960331872&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;  자바스크립트의 핵심 '비동기' 완벽 이해 ❗&quot; data-og-description=&quot;자바스크립트의 동기와 비동기 자바스크립트는 싱글 스레드 언어이기 때문에 한 번에 하나의 작업만 수행할 수 있다. 즉, 이전 작업이 완료되어야 다음 작업을 수행할 수 있게 된다. 우리가 프&quot; data-og-host=&quot;inpa.tistory.com&quot; data-og-source-url=&quot;https://inpa.tistory.com/entry/%F0%9F%8C%90-js-async#%EC%99%9C_%EC%99%84%EB%B2%BD%ED%95%9C_%EB%A9%80%ED%8B%B0_%EC%8A%A4%EB%A0%88%EB%94%A9%EC%9D%B4_%EC%95%84%EB%8B%8C%EA%B0%80&quot; data-og-url=&quot;https://inpa.tistory.com/entry/%F0%9F%8C%90-js-async&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/T9qJi/hyZqWKP3Jw/kGKNFqkNn1rLCfwhpj0c8k/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/ciopLU/hyZrnuwec2/zP7Kp7pOGXEtwiQB4VQQT0/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/b7nDXi/hyZrwrrcdn/92zs85k665afvvqxkJdw1k/img.png?width=976&amp;amp;height=508&amp;amp;face=0_0_976_508&quot;&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/%F0%9F%8C%90-js-async#%EC%99%9C_%EC%99%84%EB%B2%BD%ED%95%9C_%EB%A9%80%ED%8B%B0_%EC%8A%A4%EB%A0%88%EB%94%A9%EC%9D%B4_%EC%95%84%EB%8B%8C%EA%B0%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://inpa.tistory.com/entry/%F0%9F%8C%90-js-async#%EC%99%9C_%EC%99%84%EB%B2%BD%ED%95%9C_%EB%A9%80%ED%8B%B0_%EC%8A%A4%EB%A0%88%EB%94%A9%EC%9D%B4_%EC%95%84%EB%8B%8C%EA%B0%80&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/T9qJi/hyZqWKP3Jw/kGKNFqkNn1rLCfwhpj0c8k/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/ciopLU/hyZrnuwec2/zP7Kp7pOGXEtwiQB4VQQT0/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/b7nDXi/hyZrwrrcdn/92zs85k665afvvqxkJdw1k/img.png?width=976&amp;amp;height=508&amp;amp;face=0_0_976_508');&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;  자바스크립트의 핵심 '비동기' 완벽 이해 ❗&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트의 동기와 비동기 자바스크립트는 싱글 스레드 언어이기 때문에 한 번에 하나의 작업만 수행할 수 있다. 즉, 이전 작업이 완료되어야 다음 작업을 수행할 수 있게 된다. 우리가 프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;inpa.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1753960394641&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;Graceful asynchronous programming with Promises - Web 개발 학습하기 | MDN&quot; data-og-description=&quot;Promises 는 이전 작업이 완료될 때 까지 다음 작업을 연기 시키거나, 작업실패를 대응할 수 있는 비교적 새로운 JavaScript 기능입니다. Promise는 비동기 작업 순서가 정확하게 작동되게 도움을 줍니&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Learn_web_development/Extensions/Async_JS/Promises&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Learn_web_development/Extensions/Async_JS/Promises&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RP2sI/hyZroGWVCX/2V3v8k1O2IB2QLg2VV34l0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Learn_web_development/Extensions/Async_JS/Promises&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Learn_web_development/Extensions/Async_JS/Promises&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RP2sI/hyZroGWVCX/2V3v8k1O2IB2QLg2VV34l0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&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;Graceful asynchronous programming with Promises - Web 개발 학습하기 | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Promises 는 이전 작업이 완료될 때 까지 다음 작업을 연기 시키거나, 작업실패를 대응할 수 있는 비교적 새로운 JavaScript 기능입니다. Promise는 비동기 작업 순서가 정확하게 작동되게 도움을 줍니&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Programming Language/Javascript</category>
      <category>async</category>
      <category>await</category>
      <category>callback</category>
      <category>JavaScript</category>
      <category>MDN</category>
      <category>promise</category>
      <category>비동기</category>
      <author>devsean</author>
      <guid isPermaLink="true">https://seandailytech.tistory.com/39</guid>
      <comments>https://seandailytech.tistory.com/39#entry39comment</comments>
      <pubDate>Thu, 31 Jul 2025 20:13:45 +0900</pubDate>
    </item>
    <item>
      <title>[Java / Spring] Spring Boot 테스트 작성하기</title>
      <link>https://seandailytech.tistory.com/38</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어를 테스트하는 것은 품질 관리에 있어 중요하다. 프로젝트 수행 시, E2E 테스트 케이스를 문서로 작성하여 직접 수행해보고 통과 여부를 작성한다. 테스트를 수행하기에 앞서(혹은 수행하는 대신), 개발한 소프트웨어를 코드 레벨에서 테스트할 수 있다면 보다 효과적으로 개발할 수 있다. 장애가 발생했을 때 원인을 빠르게 파악할 수 있고, 리팩토링 시 코드가 잘 작동함을 보장할 수도 있다. 이번 포스팅에서는 간단한 Spring Boot 어플리케이션을 작성하고, 테스트 코드를 작성해보면서 방법을 익힌다.&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;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 셋팅한다. start.spring.io의 initalizer를 활용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build.gradle&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;plugins {
	id 'java'
	id 'org.springframework.boot' version '3.5.4'
	id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

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

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	runtimeOnly 'com.h2database:h2'

	implementation 'org.projectlombok:lombok:1.18.36'
	annotationProcessor 'org.projectlombok:lombok:1.18.36'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;application.yml&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내장 경량 DB인 h2 database를 활용한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  application:
    name: guestbook
  datasource:
    url: jdbc:h2:mem:guestbook
    driverClassName: org.h2.Driver
    username: admin
    password: password
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
  h2:
    console:
      enabled: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;어플리케이션 구조 및 소스코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 CRUD 어플리케이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (2).png&quot; data-origin-width=&quot;367&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxLPZ7/btsPBqIR9yd/UFMKI6ukve7RjTBBBIE191/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxLPZ7/btsPBqIR9yd/UFMKI6ukve7RjTBBBIE191/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxLPZ7/btsPBqIR9yd/UFMKI6ukve7RjTBBBIE191/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxLPZ7%2FbtsPBqIR9yd%2FUFMKI6ukve7RjTBBBIE191%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;367&quot; height=&quot;752&quot; data-filename=&quot;image (2).png&quot; data-origin-width=&quot;367&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Controller&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import com.example.demo.domain.entity.GuestbookEntity;
import com.example.demo.dto.GuestbookDto;
import com.example.demo.service.GuestbookService;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RestController
@RequestMapping(&quot;/api/guestbook&quot;)
public class GuestbookController {

    private final GuestbookService service;

    @GetMapping
    public List&amp;lt;GuestbookEntity&amp;gt; getAllGuestbooks(){
        return service.findAll();
    }

    @GetMapping(&quot;/{id}&quot;)
    public GuestbookEntity getGuestbookById(@PathVariable Long id){
        return service.findById(id);
    }

    @PostMapping
    public GuestbookEntity createGuestbook(@RequestBody GuestbookDto guestbook){
        GuestbookEntity createGuestbook = GuestbookEntity.builder()
                .name(guestbook.getName())
                .message(guestbook.getMessage())
                .build();
        return service.save(createGuestbook);
    }

    @PutMapping(&quot;/{id}&quot;)
    public GuestbookEntity updateGuestbook(@PathVariable Long id, @RequestBody GuestbookDto updatedGuestbook){
        GuestbookEntity updateGuestbook = GuestbookEntity.builder()
                .id(id)
                .name(updatedGuestbook.getName())
                .message(updatedGuestbook.getMessage())
                .build();
        return service.save(updateGuestbook);
    }

    @DeleteMapping(&quot;/{id}&quot;)
    public ResponseEntity&amp;lt;Void&amp;gt; deleteGuestbook(@PathVariable Long id){
        service.deleteById(id);
        return ResponseEntity.noContent().build();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Service&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;import com.example.demo.domain.entity.GuestbookEntity;
import com.example.demo.domain.repository.GuestbookRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@RequiredArgsConstructor
@Service
public class GuestbookService {

    private final GuestbookRepository repository;

    public List&amp;lt;GuestbookEntity&amp;gt; findAll() {
        return repository.findAll();
    }

    public GuestbookEntity findById(Long id) {
        return repository.findById(id).orElseThrow(() -&amp;gt; new ResponseStatusException(HttpStatus.NOT_FOUND, &quot;방명록 항목을 찾을 수 없습니다.&quot;));
    }

    public GuestbookEntity save(GuestbookEntity guestbook) {
        return repository.save(guestbook);
    }

    public void deleteById(Long id) {
        repository.deleteById(id);
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Repository&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;import com.example.demo.domain.entity.GuestbookEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface GuestbookRepository extends JpaRepository&amp;lt;GuestbookEntity, Long&amp;gt; {

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Entity&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
public class GuestbookEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String message;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DTO&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import lombok.*;

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
public class GuestbookDto {
    private String name = &quot;&quot;;
    private String message = &quot;&quot;;
}
&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;Maven과 Gradle은 표준 디렉토리 구조를 따르며, 테스트 코드는 기본적으로 src/test/java 아래에 위치해야 빌드 도구에서 자동으로 인식한다. 따라서 테스트 파일은 해당 경로 하위에 작성한다.&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;p data-ke-size=&quot;size16&quot;&gt;JUnit 5와 AssertJ, Mockito를 주로 활용한다. JUnit 5는 간단하고 기본적인 단위 테스트를 수행하는 데 유용한 라이브러리이며, AssertJ는 더 복잡한 테스트를 작성하는 데 활용된다. 체이닝 문법을 통해 읽기 쉬운 코드를 제공하며, 특히 다양한 타입의 객체나 조건을 비교해야 할 때 유리하다. Mockito는 단위 테스트를 수행할 때 필요한 가짜 객체(Mock 객체)를 주입 및 활용하기 위해 사용된다. 참고로 해당 라이브러리들은 spring-boot-starter-test를 build.gradle에 설정하면 자동으로 제공된다.&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;행위 기반 테스트(Given-When-Then)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는 행위 기반 테스트 방식으로, Given-When-Then 구조로 작성하는 것이 직관적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Given&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트를 시작하기 전에 주어진 조건이나 상황을 설정한다.&lt;/li&gt;
&lt;li&gt;객체 생성, 특정 값 설정, Mock 객체 설정 등의 작업을 진행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;When&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주어진 상태에서 테스트 대상 메서드를 실행한다.&lt;/li&gt;
&lt;li&gt;실제로 메서드를 호출하여 테스트하려는 행동이 일어나는 부분이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Then&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트의 결과가 예상대로 이루어졌는지 확인하는 부분이다.&lt;/li&gt;
&lt;li&gt;예상한 값과 일치하는지 확인하는 검증을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단위 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 테스트는 각 계층별로 분리하여 개별 클래스나 메서드를 테스트한다. &amp;ldquo;동작&amp;rdquo;의 관점에서 접근하며, MVC 계층 중 비즈니스 로직을 다루는 Service와 Repository를 테스트할 때 적절하다. 필요하다면 Controller 계층만 떼어서 클라이언트-서버 간의 인터페이스나 예외 처리 등을 점검할 때 활용할 수도 있다(그러나 통합 테스트를 적용하는 것이 자연스러워 보인다).&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;한편 단위 테스트에서는 실제 환경을 직접 사용하지 않고, 외부 의존성을 대체할 수 있는 가짜 객체(Mock)를 만들어 동작을 검증하게 된다. Service에 존재하는 순수한 비즈니스 로직이나, Repository에 존재하는 DB I/O 로직은 실제 작동 시에는 엮여있으나, 각각 분리되어 테스트 되어야 한다. 그러므로 테스트 시에는 필요한 의존성을 Mocking하여 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Service 계층의 단위 테스트 코드&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import com.example.demo.domain.entity.GuestbookEntity;
import com.example.demo.domain.repository.GuestbookRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

class GuestbookServiceTest {

    // Repository를 Mock 객체로 생성
    @Mock
    private GuestbookRepository guestbookRepository;

    // 테스트 대상 객체, 해당 객체가 필요한 의존성을 Mock 객체를 활용하여 주입해준다.
    @InjectMocks
    private GuestbookService guestbookService;

    private GuestbookEntity guestbookEntity;

    // 테스트 메서드 실행 직전에 필요한 공통 작업
    // Mockito 초기화 및 Entity 객체 생성하여 할당
    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
        guestbookEntity = new GuestbookEntity(1L, &quot;John Doe&quot;, &quot;Hello, World!&quot;);
    }

    @Test
    void testSaveGuestbook(){

        // given
        // when() : Mock 객체(guestbookRepository)가 특정 작업을 수행했을 때 예상되는 결과를 정의한다.
        when(guestbookRepository.save(any(GuestbookEntity.class))).thenReturn(guestbookEntity);

        // when
        GuestbookEntity savedGuestbook = guestbookService.save(guestbookEntity);

        // then
        // assert : JUnit 5의 단언문
        assertNotNull(savedGuestbook);
        assertEquals(&quot;John Doe&quot;, savedGuestbook.getName());
        assertEquals(&quot;Hello, World!&quot;, savedGuestbook.getMessage());

        // verify : Mock 객체의 동작을 검증한다. 여기서는 save 동작이 1회만 이루어졌는지를 검증한다.
        verify(guestbookRepository, times(1)).save(any(GuestbookEntity.class));

    }

    @Test
    void testGetGuestbookById() {

        // given
        when(guestbookRepository.findById(1L)).thenReturn(java.util.Optional.of(guestbookEntity));

        // when
        GuestbookEntity foundGuestbook = guestbookService.findById(1L);

        // then
        assertNotNull(foundGuestbook);
        assertEquals(&quot;John Doe&quot;, foundGuestbook.getName());
        assertEquals(&quot;Hello, World!&quot;, foundGuestbook.getMessage());

    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Repository 계층의 단위 테스트 코드&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import com.example.demo.domain.entity.GuestbookEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.junit.jupiter.api.Assertions.*;

/* @DataJpaTest
* Spring에서 제공하는 JPA 단위 테스트 Annotation
* H2 DB를 활용하여 DB I/O를 테스트한다. 
* 각 Test가 끝나면 Rollback 된다.
* */
@DataJpaTest
class GuestbookRepositoryTest {

    @Autowired
    private GuestbookRepository guestbookRepository;

    private GuestbookEntity guestbookEntity;

    @BeforeEach
    void setUp() {
        guestbookEntity = new GuestbookEntity(null, &quot;John Doe&quot;, &quot;Hello, World!&quot;);
    }

    @Test
    void testSaveGuestbook() {

        // when
        GuestbookEntity savedEntity = guestbookRepository.save(guestbookEntity);

        // then
        assertNotNull(savedEntity.getId());
        assertEquals(&quot;John Doe&quot;, savedEntity.getName());
        assertEquals(&quot;Hello, World!&quot;, savedEntity.getMessage());

    }
    
    @Test
    void testFindById() {
        
        // given
        GuestbookEntity savedEntity = guestbookRepository.save(guestbookEntity);
        
        // when
        GuestbookEntity foundEntity = guestbookRepository.findById(savedEntity.getId()).orElse(null);

        // then
        assertNotNull(foundEntity);
        assertEquals(savedEntity.getId(), foundEntity.getId());

    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;통합 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;기능&amp;rdquo;의 관점에서 접근한다. 하나의 기능이 완결되게 동작하는지를 테스트한다. 보통 서버의 입장에서는 기능을 수행하기 위해 화면으로부터 Endpoint에 요청이 들어오므로, 자연스럽게 Controller 계층을 테스트한다는 관점으로 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 애플리케이션 컨텍스트(ApplicationContext)를 로드하여 실제 API 호출이 예상대로 동작하는 지 확인한다.&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;@SpringBootTest를 사용한다. 실제 환경과 유사하지만, 애플리케이션이 클수록 테스트가 오래 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 API 호출을 테스트하므로, 애플리케이션 기동 및 요청 수행 관련하여 설정을 잡아주어야 한다. 다음과 같이 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 애플리케이션을 실제로 실행하고 무작위 포트에서 통합 테스트를 수행한다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GuestbookIntegrationTest {

    // 내장 웹 서버를 실행하는 경우, 자동으로 할당된 포트를 얻고 싶을 때 사용한다.
    @LocalServerPort
    private int port;

    // REST API를 호출하여 실제 HTTP 요청/응답을 테스트할 수 있는 유틸리티 클래스
    @Autowired
    private TestRestTemplate restTemplate;

    // 호출할 요청의 Base URL
    private String getBaseUrl(){
        return &quot;&amp;lt;http://localhost&amp;gt;:&quot; + port + &quot;/api/guestbook&quot;;
    }
    
	  //  이후 필요한 테스트 작성

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;통합 테스트 코드 전문&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import com.example.demo.domain.entity.GuestbookEntity;
import com.example.demo.dto.GuestbookDto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

// 애플리케이션을 실제로 실행하고 무작위 포트에서 통합 테스트를 수행한다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GuestbookIntegrationTest {

    // 내장 웹 서버를 실행하는 경우, 자동으로 할당된 포트를 얻고 싶을 때 사용한다.
    @LocalServerPort
    private int port;

    // REST API를 호출하여 실제 HTTP 요청/응답을 테스트할 수 있는 유틸리티 클래스
    @Autowired
    private TestRestTemplate restTemplate;

    // 호출할 요청의 Base URL
    private String getBaseUrl(){
        return &quot;&amp;lt;http://localhost&amp;gt;:&quot; + port + &quot;/api/guestbook&quot;;
    }

    @DisplayName(&quot;방명록을 생성하고 확인한다.&quot;)
    @Test
    void testCreateGuestbook() {

        /* POST 요청을 통해 방명록을 생성하고, GET 요청을 통해 잘 생성되었는지 확인한다. */

        // given
        var newEntry = GuestbookDto.builder()
                .name(&quot;홍길동&quot;)
                .message(&quot;안녕하세요!&quot;)
                .build();

        // when : POST 요청
        ResponseEntity&amp;lt;GuestbookEntity&amp;gt; createResponse = restTemplate.postForEntity(getBaseUrl(), newEntry, GuestbookEntity.class);

        // then : POST 응답 확인
        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        GuestbookEntity createdEntry = createResponse.getBody();
        assertThat(createdEntry).isNotNull();
        assertThat(createdEntry.getId()).isNotNull();
        assertThat(createdEntry.getName()).isEqualTo(&quot;홍길동&quot;);
        assertThat(createdEntry.getMessage()).isEqualTo(&quot;안녕하세요!&quot;);

        // when : GET 요청
        ResponseEntity&amp;lt;GuestbookEntity&amp;gt; getResponse = restTemplate.getForEntity(getBaseUrl() + &quot;/&quot; + createdEntry.getId(), GuestbookEntity.class);

        // then : GET 요청 확인
        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        GuestbookEntity retrievedEntry = getResponse.getBody();
        assertThat(retrievedEntry).isNotNull();
        assertThat(retrievedEntry.getId()).isEqualTo(createdEntry.getId());
        assertThat(retrievedEntry.getName()).isEqualTo(&quot;홍길동&quot;);
        assertThat(retrievedEntry.getMessage()).isEqualTo(&quot;안녕하세요!&quot;);
    }

    @DisplayName(&quot;방명록을 수정한 뒤 확인한다.&quot;)
    @Test
    void testUpdateGuestBook(){

        /* 생성된 방명록을 PUT 요청을 통해 수정하며, GET 요청을 통해 잘 수정되었는지 확인한다. */

        // given : 방명록 생성
        var newEntry = GuestbookDto.builder()
                .name(&quot;홍길동&quot;)
                .message(&quot;안녕하세요!&quot;)
                .build();

        ResponseEntity&amp;lt;GuestbookEntity&amp;gt; createResponse = restTemplate.postForEntity(getBaseUrl(), newEntry, GuestbookEntity.class);
        GuestbookEntity createdEntry = createResponse.getBody();
        assertThat(createdEntry).isNotNull();

        // when : PUT 요청을 통해 방명록 수정
        GuestbookDto updatedEntry = GuestbookDto.builder()
                .name(&quot;홍길동&quot;)
                .message(&quot;반갑습니다&quot;)
                .build();

        restTemplate.put(getBaseUrl() + &quot;/&quot; + createdEntry.getId(), updatedEntry);

        // then : GET 요청을 통해 결과 확인
        ResponseEntity&amp;lt;GuestbookEntity&amp;gt; getResponse = restTemplate.getForEntity(getBaseUrl() + &quot;/&quot; + createdEntry.getId(), GuestbookEntity.class);
        GuestbookEntity updatedEntity = getResponse.getBody();

        assertThat(updatedEntity).isNotNull();
        assertThat(updatedEntity.getId()).isEqualTo(createdEntry.getId());
        assertThat(updatedEntity.getMessage()).isEqualTo(&quot;반갑습니다&quot;);

    }

    @DisplayName(&quot;방명록을 생성한 뒤 확인한다.&quot;)
    @Test
    void testDeleteGuestbook(){

        /* 생성된 방명록을 DELETE 요청을 통해 삭제하며, GET 요청을 통해 NOT FOUND인지 확인한다. */

        // given : 방명록 생성
        var newEntry = GuestbookDto.builder()
                .name(&quot;홍길동&quot;)
                .message(&quot;안녕하세요!&quot;)
                .build();

        ResponseEntity&amp;lt;GuestbookEntity&amp;gt; createResponse = restTemplate.postForEntity(getBaseUrl(), newEntry, GuestbookEntity.class);

        GuestbookEntity createdEntry = createResponse.getBody();
        assertThat(createdEntry).isNotNull();

        // when : 방명록 삭제
        restTemplate.delete(getBaseUrl() + &quot;/&quot; + createdEntry.getId());

        // then : 삭제된 항목 조회 시 404 응답 확인
        ResponseEntity&amp;lt;GuestbookEntity&amp;gt; getResponse = restTemplate.getForEntity(getBaseUrl() + &quot;/&quot; + createdEntry.getId(), GuestbookEntity.class);
        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);

    }

    @DisplayName(&quot;생성된 모든 방명록을 확인한다.&quot;)
    @Test
    void testGetAllGuestbook() {

        // given : 방명록 여러 항목 생성
        restTemplate.postForEntity(getBaseUrl(), GuestbookDto.builder().name(&quot;홍길동&quot;).message(&quot;안녕하세요!&quot;).build(), GuestbookEntity.class);
        restTemplate.postForEntity(getBaseUrl(), GuestbookDto.builder().name(&quot;이길동&quot;).message(&quot;반갑습니다!&quot;).build(), GuestbookEntity.class);

        // when : 모든 항목 조회
        ResponseEntity&amp;lt;List&amp;gt; getResponse = restTemplate.getForEntity(getBaseUrl(), List.class);

        // then
        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        List&amp;lt;?&amp;gt; entries = getResponse.getBody();
        assertThat(entries).isNotNull();
        assertThat(entries.size()).isGreaterThanOrEqualTo(2);
    }

}&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://victorydntmd.tistory.com/353&quot;&gt;[Springboot] 테스트 코드 작성 (1) - 단위 테스트&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753760577021&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;[Springboot] 테스트 코드 작성 (1) - 단위 테스트&quot; data-og-description=&quot;Updated - 2024. 12. 08&amp;nbsp;&amp;nbsp;&amp;nbsp;테스트 코드 작성 (1) - 단위 테스트테스트 코드 작성 (2) - 통합 테스트&amp;nbsp;&amp;nbsp;이 글에서는 REST-API를 단위 테스트 코드를 작성하는 방법에 대해 알아보겠습니다.&amp;nbsp;&amp;nbsp;&amp;nbsp;01. REST-API 방&quot; data-og-host=&quot;victorydntmd.tistory.com&quot; data-og-source-url=&quot;https://victorydntmd.tistory.com/353&quot; data-og-url=&quot;https://victorydntmd.tistory.com/353&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cRtcJw/hyZrwqSDr0/c2cVhteEWucUakUwuktrg0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/PIIZi/hyZq1EH3pp/oX4TqKyyupmyAJOb4WOFN0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bX1Pqe/hyZrwdnu4S/1OkQLWe82eB1KG63rRLgtK/img.png?width=319&amp;amp;height=521&amp;amp;face=0_0_319_521&quot;&gt;&lt;a href=&quot;https://victorydntmd.tistory.com/353&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://victorydntmd.tistory.com/353&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cRtcJw/hyZrwqSDr0/c2cVhteEWucUakUwuktrg0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/PIIZi/hyZq1EH3pp/oX4TqKyyupmyAJOb4WOFN0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bX1Pqe/hyZrwdnu4S/1OkQLWe82eB1KG63rRLgtK/img.png?width=319&amp;amp;height=521&amp;amp;face=0_0_319_521');&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;[Springboot] 테스트 코드 작성 (1) - 단위 테스트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Updated - 2024. 12. 08&amp;nbsp;&amp;nbsp;&amp;nbsp;테스트 코드 작성 (1) - 단위 테스트테스트 코드 작성 (2) - 통합 테스트&amp;nbsp;&amp;nbsp;이 글에서는 REST-API를 단위 테스트 코드를 작성하는 방법에 대해 알아보겠습니다.&amp;nbsp;&amp;nbsp;&amp;nbsp;01. REST-API 방&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;victorydntmd.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://victorydntmd.tistory.com/354&quot;&gt;[Springboot] 테스트 코드 작성 (2) - 통합 테스트 (@SpringBootTest)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753760576617&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;[Springboot] 테스트 코드 작성 (2) - 통합 테스트 (@SpringBootTest)&quot; data-og-description=&quot;Updated - 2024. 12. 08&amp;nbsp;&amp;nbsp;테스트 코드 작성 (1) - 단위 테스트테스트 코드 작성 (2) - 통합 테스트&amp;nbsp;지난 글에서는 테스트 코드에 대한 전반적인 내용과 단위 테스트 코드를 작성하는 방법에 대해 알아보&quot; data-og-host=&quot;victorydntmd.tistory.com&quot; data-og-source-url=&quot;https://victorydntmd.tistory.com/354&quot; data-og-url=&quot;https://victorydntmd.tistory.com/354&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qPpTW/hyZryhWBlI/KF5G09H4f4A0g6k8glCLRK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cX0NXM/hyZqPxvep5/7aZONk5Vh2IwIot6XfkoJK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://victorydntmd.tistory.com/354&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://victorydntmd.tistory.com/354&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qPpTW/hyZryhWBlI/KF5G09H4f4A0g6k8glCLRK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cX0NXM/hyZqPxvep5/7aZONk5Vh2IwIot6XfkoJK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[Springboot] 테스트 코드 작성 (2) - 통합 테스트 (@SpringBootTest)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Updated - 2024. 12. 08&amp;nbsp;&amp;nbsp;테스트 코드 작성 (1) - 단위 테스트테스트 코드 작성 (2) - 통합 테스트&amp;nbsp;지난 글에서는 테스트 코드에 대한 전반적인 내용과 단위 테스트 코드를 작성하는 방법에 대해 알아보&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;victorydntmd.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>웹 개발/Backend</category>
      <category>Java</category>
      <category>Spring</category>
      <category>spring boot</category>
      <category>단위 테스트</category>
      <category>소프트웨어 테스트</category>
      <category>통합 테스트</category>
      <author>devsean</author>
      <guid isPermaLink="true">https://seandailytech.tistory.com/38</guid>
      <comments>https://seandailytech.tistory.com/38#entry38comment</comments>
      <pubDate>Tue, 29 Jul 2025 12:43:28 +0900</pubDate>
    </item>
  </channel>
</rss>