<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>컴퓨터 공부하는 블로그</title>
    <link>https://securityinit.tistory.com/</link>
    <description>컴퓨터 공부하는 블로그입니다.
공부하는거 다 적어요~!</description>
    <language>ko</language>
    <pubDate>Tue, 14 Apr 2026 01:41:20 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>전호영</managingEditor>
    <image>
      <title>컴퓨터 공부하는 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/5009322/attach/aa89a24501724a2280b1b836ab0160ec</url>
      <link>https://securityinit.tistory.com</link>
    </image>
    <item>
      <title>2025년 회고</title>
      <link>https://securityinit.tistory.com/268</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;2025년이 일주일 채 남지 않은 지금, 내 삶의 가장 큰 변곡점이 되었던 올해를 돌아본다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;개발을 처음 시작하며 분투했던 2024년 초반, 산악부 홈페이지를 붙들고 홀로 발버둥 쳤던 중반, 그리고 대학 생활의 정점이자 소중한 인연이 된 '큐시즘'을 만난 후반까지.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;그 치열했던 시간들이 이제는 까마득한 옛일처럼 느껴진다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;서툴기만 했던 학생의 허물을 벗고, 이제는 '사회인'이라는 이름의 새롭고도 긴 터널에 발을 들였다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;2025년은 내 인생에서 결코 잊을 수 없는 한 해가 되었음이 분명하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1~2월&lt;/h2&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;'다른 친구들은 몇 년씩 준비해 온 길인데, 길어봐야 1년, 정말 집중한 시간은 6개월 남짓인 내가 과연 취업할 수 있을까?'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;하지만 마지막 학기를 보내며 앞서 나가는 동기들을 보니 가만히 있을 수 없었다. 조급한 마음에 무작정 취준 전선에 뛰어들었다. 온라인에 있는 자소서와 포트폴리오를 참고해 나름 괜찮아 보이는(지금 보면 정말 부끄러운 수준이지만) 결과물을 만들었고, 수많은 회사에 지원서를 던졌다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;정말 운 좋게 몇몇 스타트업에서 서류 합격 소식을 들려주었다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;하지만 기초가 부족했던 내게 면접은 그저 &quot;내가 얼마나 아무것도 모르는 바보인지&quot; 확인하러 가는 시간일 뿐이었다. 이대로는 시간만 낭비할 것 같다는 판단에, 부족한 부분을 채우고자 큐시즘 연장을 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3 ~ 5월 - 취준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐시즘 31기를 시작할 땐, 조금은 이기적인 마음이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넘치는 열정으로 개발을 잘 해보겠다! 나도 개발 커뮤니티에 들어가 개발자들과 소통하고 싶다! 란 다짐으로 들어갔던 30기와 다르게, 31기는 모든 게 취업에 맞춰져 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 기업 프로젝트, 밋업 모두 취업에 도움이 되는 방향으로 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 그 선택 안에서 만난 여러 사람들은 여전히 소중하다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운이 좋게 대학 생활 도중 알게 된 친구들과 그 친구들의 인맥으로 토스 백엔드 개발자와 스터디를 진행하기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 공부를 할 때, 어느정도로 파고들어야 하는지와 나의 이력서의 단점 등 여러 부분을 배우게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상반기 공채 시즌, 끊임없이 서류를 내고 탈락하기를 반복했다. 최종 면접에서 고배를 마시기도 하고, 합격한 스타트업의 입사를 고민 끝에 미루기도 하며 불안한 줄타기를 이어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'정말 다 떨어졌구나' 싶던 절망적인 순간, 기적이 찾아왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R&amp;amp;D 관련 공공기관이었다. 공기업 준비는 해본 적도 없었고, 직무 설명서(JD)에 개발자를 뽑는다는 말만 보고 지원했을 뿐인데 서류와 논술을 덜컥 통과했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접조차 '경험 삼아 가보자, 면접비로 치킨이나 먹어야지'라는 가벼운 마음으로 임했는데 결과는 합격이었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 떨어졌겠지! 하고 열어봤던 결과, 합격이란 글자를 보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'이거 거짓말 아니겠지..? 내일이 되면 전산오류라며 다시 떨어지는 거 아닐까?'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;란 생각까지 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그런 일은 없었고...!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 입사했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝이 보이지 않던 터널이 생각보다 빠르게 끝났다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6월 ~ 9월 - 사회 초년생&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6월 2일, 정장을 차려입고 첫 출근을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준공공기관 특성상 엄숙한 분위기 속에서 나를 제외한 신입 사원들은 모두 행정직이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 가장 나이가 많을 줄 알았는데, 남자 동기 중엔 막내였고 전체에서도 나보다 어린 사람은 한 명뿐이었다.&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;그렇게 OJT 를 마치고 팀으로 이동했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 말하면 당황스러웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 업무도 하지만, 개발자는 아닌 것 같았고, 여러 업체를 관리하고, 보안 업무를 담당했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일을 한지 5~6 개월이 지난 나의 상태는 보안 컨설턴트에 조금은 더 치우쳐져 있는 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쨌든 당시엔 정말 혼란스러웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 정신없이 적응할 시간도 없이 어느새 여름이 지나갔다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9월 ~ 현재&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 지나가는지도 모르게 시간이 흘렀고, 생활에 익숙해지자 가슴 한구석에 묻어두었던 꿈이 다시 고개를 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공공기관의 삶은 평안하다. 반복적이고 고용 안정성도 뛰어나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 평온함이 나에게는 맞지 않는 옷처럼 느껴졌다. 조금 더 경쟁적인 환경에서 스스로를 몰아붙이며 전문성을 기르고 싶다는 열망이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 어릴 때부터 독일에 살고 싶었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다보니 독일로 교환학생을 가기도 했고..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대했던 것보다 더 좋았고, 그러다 보니 자연스레 독일에 살고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 지금은?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;독일에 가서 살아보는 것이 내 새로운 목표가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;2026년은 독일로 가기 위한 준비를 하며 살아가지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&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년이었다. 2026년은 독일로 가기 위한 내실을 다지며, 올해보다는 조금 더 잔잔하고 단단하게 준비하는 한 해가 되기를 바란다.&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;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>✨ 회고</category>
      <author>전호영</author>
      <guid isPermaLink="true">https://securityinit.tistory.com/268</guid>
      <comments>https://securityinit.tistory.com/268#entry268comment</comments>
      <pubDate>Sun, 28 Dec 2025 13:27:13 +0900</pubDate>
    </item>
    <item>
      <title>사내 근태 시스템(출&amp;middot;퇴근) asp에서 NestJS 로 마이그레이션 후기</title>
      <link>https://securityinit.tistory.com/267</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 재직 중인 회사의 사내 근태(출&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&amp;middot;퇴근, 외근, 출장 등)의 경우 다음과 같은 구조를 가진다.&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;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;사내 DB -&amp;gt; 외주 근태 서비스 -&amp;gt; 사내 DB 업데이트&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&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;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;출&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&amp;middot;퇴근의 경우 캡스에 출근을 찍으면, 해당 데이터가 DB에 저장된다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;그 후 DB에 있는 값을 외부 근태 서비스로 전달하고, 근태 서비스에 반영되면 사내 DB에 근태 전송 완료 플래그를 업데이트한다.&lt;/span&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;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;얼레벌레 돌아가는 서비스를 마이그레이션 하겠단 결정을 한 이유는 다음과 같다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&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;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;1.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;회사 내부의 대부분 코드가 asp로 적혀있었고, 로깅도 하지 않았기에 문제가 발생하면 어디서 문제가 발생했는지 찾기 힘들었다. 추가로 급하게 짜인 코드라 그런지 의미 없는 변수명, 꼬여있는 로직 등으로 코드에 대한 분석이 쉽지 않았다.&lt;/span&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;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;2. asp 코드의 특성상 코드 수정 후 저장하면 저장사항이 바로 반영된다. 서버에 들어올 수 있는 사람이 여러 명(외주 업체)이었기에 변경사항이 바로 서비스에 반영되는 특성은 서비스의 안정성을 매우 떨어뜨렸다.&lt;/span&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;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;3. 내가 asp에 익숙하지 않고, asp에 익숙한 사람이 거의 없어지고 있다. 내가 더 편하게, 나 이후에 들어 올 사람들이 더 편하게 일하길 바라는 마음에 마이그레이션을 결정했다.&lt;/span&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;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;그러므로 asp로 되어있는 사내 근태 서비스를 기능 단위로 NestJS로 전환하려 한다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 기존 서비스 로직 분석&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&amp;middot;퇴근 서비스는 다음과 같은 로직으로 돌아간다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;MS-SQL 내부 테이블에서 출퇴근 데이터를 조회한다. 조회 시 날짜와 외부 근태 서비스에 반영이 됐는지 여부를 통해 데이터를 호출한다.&lt;/li&gt;
&lt;li&gt;외주 API에 맞게 데이터를 가공한다.&lt;/li&gt;
&lt;li&gt;외주 API를 호출한다.&lt;/li&gt;
&lt;li&gt;응답에 따라 내부 테이블을 업데이트한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 로직을 1분마다 반복하는 것이 내부 로직이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 마이그레이션을 해보자!&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 기존 코드가 가지고 있는 문제점을 정리해 보았다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;하드코딩 되어있는 정보&lt;/li&gt;
&lt;li&gt;의미 없는 변수명과 파일명&lt;/li&gt;
&lt;li&gt;변경사항이 바로 적용되는 문제&lt;/li&gt;
&lt;li&gt;공통 로직 분리가 되어있지 않아 반복되는 코드&lt;/li&gt;
&lt;li&gt;전혀 없는 보안&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 문제를 해결하기 위한 해결책을 생각해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 하드코딩 되어있는 정보&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 DB 서버에 대한 정보(&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;서버 ip 주소, id, pw 등&lt;span&gt; )&lt;/span&gt;&lt;/span&gt;가 그대로 노출되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 값은 전부 환경변수로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 의미 없는 변수명과 파일명&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;attendance1.asp, attendance2.asp 등 비슷한 이름의 여러 파일과 a, b 등의 여러 변수명이 섞여있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 부분은 ai를 통해 분석했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asp 코드 내 변수명 옆에 주석을 달아 해당 변수들이 어떤 역할을 하는지 표기했고, 이를 바탕으로 마이그레이션을 진행했다.&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;b&gt;3. 변경사항이 바로 적용되는 문제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트 언어가 가지고 있는 공통적인 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경사항을 저장하면 바로 코드에 반영된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Docker로 띄울 생각을 했지만 윈도 서버 버전이 낮아 Docker가 돌아가지 않았다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 PM2를 사용하기로 결정했다.&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;b&gt;4. 공통 로직 분리가 되어있지 않아 반복되는 코드&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외주 서비스로 요청을 보내는 코드들이 반복적으로 들어있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 코드가 많다 보니 한 파일에서 로직을 수정하면 같은 코드를 여러 번 수정해야 하는 문제가 있었다.&lt;/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;b&gt;5. 에러발생 시 즉각적으로 알 수 없고, 로깅이 안 되는 문제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 에러가 발생한 경우, 사용자가 직접 문제를 겪고 나서 우리에게 알려주게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 발생 시 알림이 없고, 로깅도 안되기에 어디서 어떤 문제로 에러가 발생했는지 알 수 없다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;애로 사항&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;공통 로직 분리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 분리를 하기 위해 다음과 같이 폴더 구조를 구성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1763279220299&quot; class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;src/
├── main.ts                                 
├── app.module.ts                           
│
├── database/                               # 데이터베이스 레이어 (Raw Query만)
│   ├── oracle/                             # Oracle 연결 관리
│   │   ├── oracle.provider.ts              # Connection Pool Provider
│   │   └── oracle.service.ts               # Raw Query 래퍼
│   ├── mssql/                              # MS-SQL 연결 관리
│   │   ├── mssql.provider.ts               # Connection Pool Provider
│   │   └── mssql.service.ts                # Raw Query 래퍼
│   └── types/                              # 타입 정의
│       ├── oracle.types.ts                 # Oracle 테이블 타입
│       └── mssql.types.ts                  # MS-SQL 테이블 타입
│
├── modules/                                # 기능별 모듈
│   ├── attendance/                         # 출장 관리 모듈
│   │   ├── attendance.module.ts
|   |   ├── attendance.facade.ts            # 서비스 메서드를 조합할 Facade
│   │   ├── attendacne.service.ts           # 비즈니스 로직 + Raw SQL
│   │   ├── attendance.controller.ts        # REST API 엔드포인트
│   │   └── dto/                            # 데이터 전송 객체
│   │       ├── sync-attendance.dto.ts
│   │       └── update-business-trip.dto.ts
│   │
│   ├── vacation/                           # 휴가 관리 모듈
│   │   ├── vacation.module.ts
|   |   ├── vacation.facade.ts    
│   │   ├── vacation.service.ts
│   │   ├── vacation.controller.ts
│   │   └── dto/
│   │&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;사내 서비스와 외주 서비스의 동기화를 위해 값들을 모두 외주 서비스 API에 요청을 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 외주 서비스에 요청을 보내는 부분을 모듈로 분리했다.&lt;/p&gt;
&lt;pre id=&quot;code_1763280728070&quot; class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

export interface AttendanceParams {
  employeeId: string; // 직원 ID
  workDate: string; // 근무 일자 (YYYYMMDD)
  type: string; // 출퇴근 구분
  date: string; // 일자
  time: string; // 시간 (HHMM 형식)
}

@Injectable()
export class ExternalApiService {
  private readonly logger = new Logger(ExternalApiService.name);
  private readonly baseUrl: string;
  private readonly apiKey: string;
  private readonly tenantKey: string;

  constructor(private readonly configService: ConfigService) {
    this.baseUrl = this.configService.get&amp;lt;string&amp;gt;('EXTERNAL_API_BASE_URL');
    this.apiKey = this.configService.get&amp;lt;string&amp;gt;('EXTERNAL_API_KEY');
    this.tenantKey = this.configService.get&amp;lt;string&amp;gt;('EXTERNAL_TENANT_KEY');
  }

  /**
   * 외부 API 공통 호출 메서드
   * @param endpoint - API 엔드포인트
   * @param params - 요청 파라미터
   * @returns API 응답
   */
  private async request&amp;lt;T = any&amp;gt;(
    endpoint: string,
    params: Record&amp;lt;string, any&amp;gt;,
  ): Promise&amp;lt;T&amp;gt; {
    const url = new URL(`${this.baseUrl}${endpoint}`);

    // URL 파라미터 추가
    Object.keys(params).forEach((key) =&amp;gt;
      url.searchParams.append(key, params[key]),
    );

    try {
      const res = await fetch(url, {
        method: 'GET',
        headers: {
          tenantKey: this.tenantKey,
          apiKey: this.apiKey,
        },
      });

      if (!res.ok) {
        this.logger.error(
          `외부 API 호출 실패: ${endpoint}, 상태 코드: ${res.status}`,
        );
        throw new Error(`네트워크 응답 오류: ${res.statusText}`);
      }

      const data = await res.json();
      return data;
    } catch (error) {
      this.logger.error(`외부 API 호출 중 에러 발생: ${endpoint}`, error);
      throw error;
    }
  }

  /**
   * 출퇴근 기록 전송
   * @param params - 출퇴근 기록 파라미터
   */
  async sendAttendance(params: AttendanceParams): Promise&amp;lt;any&amp;gt; {
    this.logger.log(
      `출퇴근 기록 전송: 직원 ID ${params.employeeId}, 날짜 ${params.workDate}, 구분 ${params.type === '1' ? '출근' : '퇴근'}`,
    );

    try {
      const result = await this.request('/attendance/record', params);
      this.logger.log(
        `출퇴근 기록 전송 성공: 직원 ID ${params.employeeId}, 날짜 ${params.workDate}`,
      );
      return result;
    } catch (error) {
      this.logger.error(
        `출퇴근 기록 전송 실패: 직원 ID ${params.employeeId}, 날짜 ${params.workDate}, 구분 ${params.type}`,
        error,
      );
      throw error;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ORM 사용 여부&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 대부분의 경우 ORM을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 마이그레이션에도 ORM을 사용하려 했지만.... 문제가 많았다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 내 DB 서버로 Oracle과 &lt;span style=&quot;background-color: #ffffff; color: #0a0a0a; text-align: start;&quot;&gt;MSSQL을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0a0a0a; text-align: start;&quot;&gt;문제는 MSSQL 버전이 너무 낮아 이를 지원하는 ORM이 없었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0a0a0a; text-align: start;&quot;&gt;추가로 Node.js의 경우 ORM이 매우 많고, 스프링과 다르게 천하통일된 ORM이 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0a0a0a; text-align: start;&quot;&gt;그러므로 만약 ORM 개발자가 개발을 멈추면 서비스에 큰 장애가 생길 수 있었다.&amp;nbsp;&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;background-color: #ffffff; color: #0a0a0a; text-align: start;&quot;&gt;그러다 보니 Raw Query에 대한 래퍼 코드를 직접 만들어&amp;nbsp;Raw Query를 그대로 사용하기로 결정했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Raw Query를 사용함에 있어 큰 문제가 되는 것이 SQL Injection이었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 Prepared Statement를 사용했다.&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;Prepared Statement란 SQL 쿼리를 &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;pre id=&quot;code_1763280012137&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- SQL 템플릿을 먼저 데이터베이스에 전송
SELECT * FROM users WHERE username = ? AND password = ?&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 단계에선 실제 값을 바인딩한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 컴파일된 쿼리에 파라미터만 전달하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 SQL과 Prepared Statement를 비교하면 이렇다.&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;일반 SQL&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const sql = &quot;SELECT * FROM users WHERE id = &quot; + userInput;
// userInput = &quot;1 OR 1=1&quot; 입력 시
// 실행: SELECT * FROM users WHERE id = 1 OR 1=1  (모든 데이터 노출!)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;Prepared Statement&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const sql = &quot;SELECT * FROM users WHERE id = ?&quot;;
pstmt.setString(1, &quot;1 OR 1=1&quot;);
// 실행: SELECT * FROM users WHERE id = '1 OR 1=1'  (문자열로 처리)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터 값이 &lt;b&gt;SQL 명령어가 아닌 데이터로만 처리&lt;/b&gt;되기 때문에 SQL Injection이 불가능해진다.&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;이 외에도 Preapred Statement가 가지는 성능 이점 등 여러 장점이 있다.&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;Raw Query 를 사용하므로 쿼리 래퍼를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763279202140&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Injectable, Inject, OnModuleDestroy } from '@nestjs/common';
import * as sql from 'mssql';
import { MSSQL_POOL } from './mssql.provider';

@Injectable()
export class MssqlService implements OnModuleDestroy {
  constructor(@Inject(MSSQL_POOL) private pool: sql.ConnectionPool) {}

  /**
   * SELECT 쿼리 실행
   */
  async query&amp;lt;T = any&amp;gt;(
    sqlQuery: string,
    params: { [key: string]: any } = {},
  ): Promise&amp;lt;T[]&amp;gt; {
    try {
      const request = this.pool.request();

      // 파라미터 바인딩
      for (const [key, value] of Object.entries(params)) {
        request.input(key, value);
      }

      const result = await request.query(sqlQuery);
      return result.recordset as T[];
    } catch (error) {
      console.error('MS-SQL 쿼리 실패:', error);
      throw error;
    }
  }

  /**
   * INSERT/UPDATE/DELETE 실행
   */
  async execute(
    sqlQuery: string,
    params: { [key: string]: any } = {},
  ): Promise&amp;lt;sql.IResult&amp;lt;any&amp;gt;&amp;gt; {
    try {
      const request = this.pool.request();

      // 파라미터 바인딩
      for (const [key, value] of Object.entries(params)) {
        request.input(key, value);
      }

      const result = await request.query(sqlQuery);
      console.log(`✅ ${result.rowsAffected[0]} 행이 영향을 받았습니다.`);

      return result;
    } catch (error) {
      console.error('MS-SQL execute 실패:', error);
      throw error;
    }
  }

  /**
   * 트랜잭션 실행
   */
  async transaction&amp;lt;T&amp;gt;(
    callback: (transaction: sql.Transaction) =&amp;gt; Promise&amp;lt;T&amp;gt;,
  ): Promise&amp;lt;T&amp;gt; {
    const transaction = new sql.Transaction(this.pool);

    try {
      await transaction.begin();

      const result = await callback(transaction);

      await transaction.commit();
      console.log('✅ 트랜잭션 커밋 완료');

      return result;
    } catch (error) {
      await transaction.rollback();
      console.warn('⚠️ 트랜잭션 롤백 완료');
      throw error;
    }
  }

  /**
   * 앱 종료 시 Pool 정리
   */
  async onModuleDestroy() {
    try {
      await this.pool.close();
      console.log('✅ MS-SQL Pool 종료');
    } catch (error) {
      console.error('MS-SQL Pool 종료 실패:', 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;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 다음 부분이 파라미터 바인딩(Prepared Statement)에 해당한다.&lt;/p&gt;
&lt;pre id=&quot;code_1763280326839&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  // 파라미터 바인딩
  for (const [key, value] of Object.entries(params)) {
    request.input(key, value);
  }&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;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;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;빠르게 마이그레이션을 해야 했고, 최대한 비용을 절감해야 하기에 winston을 사용해 로그 파일을 저장하고, 사내 메신저인 잔디로 알림을 보내기로 했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Winston을 전역 로거로 설정했다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 단일 서버를 사용하고 있었기에, ELK, Datadog과 같은 서비스를 붙이기엔 오버스펙이라 판단했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Winston을 사용할 경우, nest-winston 같은 라이브러리를 사용해 빠르게 우리 서비스와 통합할 수 있었고, 무료, 잔디 연동 등 여러 장점이 있기에 Winston을 사용했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.&amp;nbsp;로그를&amp;nbsp;여러&amp;nbsp;곳으로&amp;nbsp;전송한다.&lt;/b&gt;&lt;br /&gt;개발 도중에 봐야 할 로그, 전체 로그, 에러 저장 로그, 실시간 알림(잔디) 이렇게 한 번 로그를 찍으면 4 곳에 로그를 찍거나 저장해야 했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러므로 winston-transport 라이브러리를 사용해 로그를 관리했다.&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;Console Transport - 개발 중 실시간 확인용&lt;/b&gt;&lt;br /&gt;- 모든 레벨의 로그 출력&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DailyRotateFile (일반 로그) - 전체 로그 저장&lt;/b&gt;&lt;br /&gt;- 날짜별 파일 자동 생성 (app-2025-01-15.log)&lt;br /&gt;- 14일 보관, 자동 압축&lt;br /&gt;- 디버깅 시 전체 흐름 파악 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DailyRotateFile (에러 로그) - 에러만 별도 관리&lt;/b&gt;&lt;br /&gt;- error 레벨만 필터링&lt;br /&gt;- 30일 보관 (일반 로그보다 오래 보관)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Custom Jandi Transport - 실시간 알림&lt;/b&gt;&lt;br /&gt;- warn, error 레벨만 전송&lt;br /&gt;- 에러 발생 시 바로 알 수 있도록&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Exception Filter로 에러를 통합 처리한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Exception Filter를 사용해 모든 에러를 잡아 한 번의 Winston 로깅으로 콘솔 출력, 파일 저장, 잔디 알림을 구현했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQVJGu/dJMcabP6F8s/98qC02qhpUOJNABNmbwSj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQVJGu/dJMcabP6F8s/98qC02qhpUOJNABNmbwSj0/img.png&quot; data-alt=&quot;제대로 전송이 된다!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQVJGu/dJMcabP6F8s/98qC02qhpUOJNABNmbwSj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQVJGu%2FdJMcabP6F8s%2F98qC02qhpUOJNABNmbwSj0%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;251&quot; height=&quot;454&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2114&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;제대로 전송이 된다!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 사용하고 있는 근태 시스템엔 여러 모듈이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 그 중 출&amp;middot;퇴근만 NestJS로 마이그레이션 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 모든 모듈을 NestJS로 마이그레이션 하고, 그 과정을 남겨보도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>✨ 회고</category>
      <author>전호영</author>
      <guid isPermaLink="true">https://securityinit.tistory.com/267</guid>
      <comments>https://securityinit.tistory.com/267#entry267comment</comments>
      <pubDate>Sun, 16 Nov 2025 19:54:29 +0900</pubDate>
    </item>
    <item>
      <title>Java의 Optional을 Typescript로 구현하기</title>
      <link>https://securityinit.tistory.com/266</link>
      <description>&lt;figure id=&quot;og_1761980233001&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;타입스크립트에서 null을 처리하는 방법&quot; data-og-description=&quot;Java로 개발할 때 가장 중요하게 여겼던 것은 Null에 대한 처리이다.특히 NullPointerException(이하 NPE)이 발생하면 우리 서비스가 갑자기 종료될 수 있기에 사용자 경험에 매우 좋지 않다. 타입스크립&quot; data-og-host=&quot;securityinit.tistory.com&quot; data-og-source-url=&quot;https://securityinit.tistory.com/265&quot; data-og-url=&quot;https://securityinit.tistory.com/265&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/8v2Vh/hyZMJ48p2Q/Ol1EndWCnPDUYI26kVmmS0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/baExTT/hyZLfjCZJZ/C2099onJtKDa1t2n90YnZ0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://securityinit.tistory.com/265&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://securityinit.tistory.com/265&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/8v2Vh/hyZMJ48p2Q/Ol1EndWCnPDUYI26kVmmS0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/baExTT/hyZLfjCZJZ/C2099onJtKDa1t2n90YnZ0/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;타입스크립트에서 null을 처리하는 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Java로 개발할 때 가장 중요하게 여겼던 것은 Null에 대한 처리이다.특히 NullPointerException(이하 NPE)이 발생하면 우리 서비스가 갑자기 종료될 수 있기에 사용자 경험에 매우 좋지 않다. 타입스크립&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;securityinit.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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;[함수 호출 -&amp;gt; null 체크 -&amp;gt; 실패하면 null 반환 -&amp;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;/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;Kotlin의 let 체이닝, Java의 Optional 등으로 처리할 수 있지만, Typescript에선 방법이 따로 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Typescript에선 if (null) return null;이라는 구조를 계속 반복했었다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Java의 Optional&amp;lt;T&amp;gt; 과 Kotlin의 let&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 Optional&amp;lt;T&amp;gt; , Kotlin의 let 이 두 문법은 이런 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Null 일 수도 있는 값을 컨테이너에 담는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 안에 &lt;b&gt;값이 있을 경우에만 다음 함수를 실행&lt;/b&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;특히 Java Optional의 경우, 래퍼 타입으로 클래스이다.&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;Typescript로 Java의 Optional과 비슷한 타입을 구현해 보자!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Option 타입 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1761982277739&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type Nullable&amp;lt;T&amp;gt; = T | null | undefined;

export interface IOption&amp;lt;T&amp;gt; {
  // 값이 있는지 확인
  isPresent(): boolean;

  // 값이 있는 경우 함수 적용하고 그 결과를 Option으로 래핑 (Java의 map)
  map&amp;lt;U&amp;gt;(fn: (value: T) =&amp;gt; U): IOption&amp;lt;U&amp;gt;;

  // 값이 있는 경우 함수 적용하고 그 결과는 Option 이어야 함. (Java의 flatMap)
  flatMap&amp;lt;U&amp;gt;(fn: (value: T) =&amp;gt; IOption&amp;lt;U&amp;gt;): IOption&amp;lt;U&amp;gt;;

  // 값이 없으면 기본값 반환
  orElse&amp;lt;D&amp;gt;(defaultValue: D): T | D;

  // 값이 있으면 그 값을 반환하고, 없으면 null 반환
  getOrElse&amp;lt;D&amp;gt;(defaultValue: D): T | D;
}&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;위와 같이 Java의 Optional &amp;lt;T&amp;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;이제 값이 있는 경우(some)와 없는 경우(none), 위 인터페이스를 상속받아 클래스를 구현해 보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;None 상태 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;None은 값이 없는 상태이다.&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;pre id=&quot;code_1761983176403&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { IOption } from &quot;./IOption&quot;;

class None implements IOption&amp;lt;never&amp;gt; {
  // 싱글톤
  static readonly INSTANCE: None = new None();

  private constructor() {}

  isPresent(): boolean {
    return false;
  }

  map&amp;lt;U&amp;gt;(_fn: (value: never) =&amp;gt; U): IOption&amp;lt;U&amp;gt; {
    return None.INSTANCE as IOption&amp;lt;U&amp;gt;;
  }

  flatMap&amp;lt;U&amp;gt;(_fn: (value: never) =&amp;gt; IOption&amp;lt;U&amp;gt;): IOption&amp;lt;U&amp;gt; {
    return None.INSTANCE as IOption&amp;lt;U&amp;gt;;
  }

  orElse&amp;lt;D&amp;gt;(defaultValue: D): never | D {
    return defaultValue;
  }

  getOrElse&amp;lt;D&amp;gt;(defaultValue: D): never | D {
    return defaultValue;
  }
}

// 쉽게 None 클래스의 인스턴스를 사용하기 위함
export const none = &amp;lt;T&amp;gt;(): IOption&amp;lt;T&amp;gt; =&amp;gt; None.INSTANCE as IOption&amp;lt;T&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;왜 never 타입으로 정의했을까?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Typescript에서 never란 &quot;&lt;b&gt;절대 발생할 수 없는 값&lt;/b&gt;&quot;을 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;never타입엔 어떤 값도 할당할 수 없다.&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;지금 우리가 구현하는 None은 &lt;b&gt;값이 비어있는 상태&lt;/b&gt;를 의미한다.(null 또는 undefined)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, string이든, number 든 어떤 타입의 값도 갖고 있지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 None 내부에서 값을 꺼내려고 실행하면 안 되고, 그런 경우는 막아줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1761983805107&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// None은 내부적으로 T 타입의 값을 가지지만,
// 그 T 타입이 '절대 존재하지 않는 타입 (never)'이다.
class None implements IOption&amp;lt;never&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;이렇게 정의하면, None 인스턴스 내부에서 값을 꺼내려고 시도하거나, 그런 메서드가 있다면, 타입 체커가 해당 값이 never임을 알고 컴파일 에러를 뱉는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 타입 안정성을 가져갈 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 싱글톤 패턴을 사용해 메모리 효율성을 높일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;None 은 IOption&amp;lt;string&amp;gt;이든 IOption&amp;lt;number&amp;gt; 든 값이 없는 상태이므로 항상 동일하다.&lt;/p&gt;
&lt;pre id=&quot;code_1761983992648&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const emptyString = none&amp;lt;string&amp;gt;(); // IOption&amp;lt;string&amp;gt; (실제로는 None.INSTANCE)
const emptyNumber = none&amp;lt;number&amp;gt;(); // IOption&amp;lt;number&amp;gt; (실제로는 None.INSTANCE)&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;위와 같은 경우엔 실제로 두 값 모두 메모리상 동일한 None 객체를 참조한다.&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;never 타입은 모든 타입의 서브 타입이다.&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;즉, 값이 없는 모든 경우, T 타입을 never로 캐스팅할 수 있다.&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;IOption&amp;lt;T&amp;gt; 로 정의한 수많은 변수를 사용할 때 값이 없는 경우가 오면, 전부 하나의 None.INSTANCE를 참조하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 메모리 효율성도 챙길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Some 상태 구현&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1761984928372&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { IOption } from '../types';

class Some&amp;lt;T&amp;gt; implements IOption&amp;lt;T&amp;gt; {
  constructor(private readonly value: T) {
  }

  isPresent(): boolean {
    return true;
  }

  map&amp;lt;U&amp;gt;(fn: (value: T) =&amp;gt; U): IOption&amp;lt;U&amp;gt; {
    return new Some(fn(this.value));
  }

  flatMap&amp;lt;U&amp;gt;(fn: (value: T) =&amp;gt; IOption&amp;lt;U&amp;gt;): IOption&amp;lt;U&amp;gt; {
    return fn(this.value);
  }

  orElse&amp;lt;D&amp;gt;(_defaultValue: D): T | D {
    return this.value;
  }

  getOrElse&amp;lt;D&amp;gt;(_defaultValue: D): T | D {
    return this.value;
  }
}

export const some = &amp;lt;T&amp;gt;(value: T): IOption&amp;lt;T&amp;gt; =&amp;gt; new Some(value);&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;Java에서 Optional.ofNullable()는 매우 자주 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nullable 값을 Optional 타입으로 변환해 주는 메서드인데, 동일하게 구현해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762052691992&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { IOption, Nullable } from './types';
import { none } from './implementation/none';
import { some } from './implementation/some';

export const of = &amp;lt;T&amp;gt;(value: Nullable&amp;lt;T&amp;gt;): IOption&amp;lt;T&amp;gt; =&amp;gt; {
  if (value === null || value === undefined) {
    return none();
  }
  return some(value);
}&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;이렇게 null 또는 undefined 값이 들어온 경우, 실제 값이 있는 경우 모두 of를 통해 처리할 수 있게 되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실제 적용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 우리가 이전 글에서 문제 삼았던 코드를 정리해 보자!&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762052899461&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function processUserData(userId: string) {
  const user = findUser(userId);
  if (!user) return null;
  
  const validated = validateUser(user);
  if (!validated) return null;
  
  const enriched = enrichUserData(validated);
  if (!enriched) return null;
  
  const formatted = formatForDisplay(enriched);
  if (!formatted) return null;
  
  return formatted;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 코드가 다음과 같이 정리된다.&lt;/p&gt;
&lt;pre id=&quot;code_1762053172445&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { of } from './option';

function findUser(userId: string) : User | null {}
function validateUser(user: User) : ValidatedUser | null {}
function enrichUserData(user: ValidatedUser) : EnrichedUser | null {}
function formatForDisplay(user: EnrichedUser) : FormattedUser | null {}

function processUser(userId: string): FormattedData | null {
  return of(findUser(userId))
      .flatMap(user =&amp;gt; of(validateUser(user)))
      .flatMap(validUser =&amp;gt; of(enrichUserData(validUser)))
      .flatMap(enrichedUser =&amp;gt; of(formatForDisplay(enrichedUser)))
      .getOrElse(null);
}&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;각 체인에서 메서드들(findUser, validateUser 등)이 null을 반환할 수 있으므로, flatMap을 사용해줘야 한다!&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;만약 map을 사용한다면 null 값이 반한 되더라도, 그대로 IOption&amp;lt;T&amp;gt; 컨테이너 안에 들어가게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 타입이 IOption&amp;lt;T | null&amp;gt; 상태가 되고, Some 상태로 판단하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null 체크가 깨져버린다.&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;flatMap의 경우 IOption&amp;lt;T&amp;gt;를 반환하므로 null 값이 들어온다면 바로 none이 반환되므로 체이닝 중간에 null 또는 undefined 값이 들어왔음을 알고 체이닝을 종료시킨다.&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;이렇게 커스텀 Option을 직접 구현해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이러한 구조를 모나드 패턴이라 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 래핑해 컨테이너에 넣고 여러 연산을 체이닝 할 수 있게 해주는 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에선 타입스크립트에 숨어있는 모나드 패턴에 대해 더 알아보자!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1762054145934&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;[Scala] 예외 처리 ( Option, Either, Try ) - Data Engineer&quot; data-og-description=&quot;1. 스칼라 예외처리 Scala에서는 JVM 기반 언어 최대의 적인 NPE(NullPointerException)를 functional하게 handling 할 수 있는 다양한 수단을 제공하고 있다. Scala의 exception handling 3인방인 Option, Either, Try 에 대해&quot; data-og-host=&quot;wonyong-jang.github.io&quot; data-og-source-url=&quot;https://wonyong-jang.github.io/scala/2021/04/29/Scala-Option-Either-Try.html&quot; data-og-url=&quot;https://wonyong-jang.github.io/scala/2021/04/29/Scala-Option-Either-Try.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/by3aWg/hyZMOZYgoU/fChF1zDXpryymcKv5i1By0/img.png?width=1156&amp;amp;height=384&amp;amp;face=0_0_1156_384,https://scrap.kakaocdn.net/dn/UnS0f/hyZMQ4xVff/AicyvkSTrTW5EcUKVYKGn1/img.png?width=992&amp;amp;height=408&amp;amp;face=0_0_992_408,https://scrap.kakaocdn.net/dn/bJ8O2j/hyZMYBwSDU/KJXLswEhnMEaKSsJTuzbAK/img.png?width=866&amp;amp;height=418&amp;amp;face=0_0_866_418&quot;&gt;&lt;a href=&quot;https://wonyong-jang.github.io/scala/2021/04/29/Scala-Option-Either-Try.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wonyong-jang.github.io/scala/2021/04/29/Scala-Option-Either-Try.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/by3aWg/hyZMOZYgoU/fChF1zDXpryymcKv5i1By0/img.png?width=1156&amp;amp;height=384&amp;amp;face=0_0_1156_384,https://scrap.kakaocdn.net/dn/UnS0f/hyZMQ4xVff/AicyvkSTrTW5EcUKVYKGn1/img.png?width=992&amp;amp;height=408&amp;amp;face=0_0_992_408,https://scrap.kakaocdn.net/dn/bJ8O2j/hyZMYBwSDU/KJXLswEhnMEaKSsJTuzbAK/img.png?width=866&amp;amp;height=418&amp;amp;face=0_0_866_418');&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;[Scala] 예외 처리 ( Option, Either, Try ) - Data Engineer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 스칼라 예외처리 Scala에서는 JVM 기반 언어 최대의 적인 NPE(NullPointerException)를 functional하게 handling 할 수 있는 다양한 수단을 제공하고 있다. Scala의 exception handling 3인방인 Option, Either, Try 에 대해&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;wonyong-jang.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category> ️ 내가 다시 볼 것</category>
      <category>모나드</category>
      <author>전호영</author>
      <guid isPermaLink="true">https://securityinit.tistory.com/266</guid>
      <comments>https://securityinit.tistory.com/266#entry266comment</comments>
      <pubDate>Sun, 2 Nov 2025 12:23:10 +0900</pubDate>
    </item>
    <item>
      <title>타입스크립트에서 null을 처리하는 방법</title>
      <link>https://securityinit.tistory.com/265</link>
      <description>&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java로 개발할 때 가장 중요하게 여겼던 것은 &lt;b&gt;Null에 대한 처리&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;b&gt;NullPointerException&lt;/b&gt;(이하 &lt;b&gt;NPE&lt;/b&gt;)이 발생하면 우리 서비스가 갑자기 종료될 수 있기에 사용자 경험에 매우 좋지 않다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트/자바스크립트에도 Java의 NPE와 비슷한 &lt;b&gt;Cannot read properties of undefined&lt;/b&gt; 오류가 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 오류는 NPE만큼이나 치명적이다. 프론트엔드에서 이 오류가 발생하면 사용자 화면이 하얗게 변하거나, 특정 기능이 작동하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서 발생하면 API 응답이 실패하고 전체 요청이 무산된다.(NPE와 동일하다.)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 큰 문제는 JavaScript의 특성상 &lt;b&gt;undefined&lt;/b&gt;와 &lt;b&gt;null&lt;/b&gt;이라는 두 가지 '없음'이 공존한다는 점이다. Java 개발자 입장에서는 혼란스러울 수 있지만, TypeScript를 제대로 활용하면 이 문제를 컴파일 타임에 잡아낼 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript는 Java와 다르게 &lt;b&gt;undefined&lt;/b&gt;와 &lt;b&gt;null&lt;/b&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;undefined와 null은 도대체 뭐가 다를까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;자바스크립트 undefined와 null의 차이&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;값&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;의도&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;타입(typeof)&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;예시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;undefined&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;값이 할당되지 않음(변수는 선언되었지만 초기화되지 않음)&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;'udefined'&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;- 변수를 선언만 했을 경우&lt;br /&gt;- 객체에 없는 속성에 접근시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;null&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;값이 의도적으로 비어 있음(개발자가 명시적으로 빈 값을 할당)&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;'object'&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;- 명시적으로 객체를 비울 때&lt;br /&gt;- 함수가 유효하지 않은 값을 반환할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한듯 다르다.&lt;/p&gt;
&lt;pre id=&quot;code_1760843428080&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. undefined: 값이 할당되지 않음
let name;
console.log(name);              // 출력: undefined
console.log(typeof name);       // 출력: &quot;undefined&quot;

const user = { username: &quot;Alice&quot; };
console.log(user.age);          // 출력: undefined (속성이 없음)

// 2. null: 명시적으로 비어 있는 값
let data = null;
console.log(data);              // 출력: null
console.log(typeof data);       // 출력: &quot;object&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 초기 언어 제작 시 발생한 문제로, 현재까지 고쳐지지 않는다.&lt;/p&gt;
&lt;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;js 계열에선 두 가지 비교 연산자를 사용한다.&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;null과 undefined를 동등하다고 간주한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760843551375&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;console.log(null == undefined); // 출력: true (타입 변환 후 동등하다고 간주)
console.log(null == 0);         // 출력: false
console.log(undefined == 0);    // 출력: false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=== 는 엄격하게 비교한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 말이냐면, &lt;b&gt;타입과 값을 모두 비교&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 두 값은 서로 다르게 취급된다.&lt;/p&gt;
&lt;pre id=&quot;code_1760843704877&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;console.log(null === undefined); // 출력: false (타입이 다르므로)&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;그러다 보니 js로 개발을 하면 대부분 === 를 사용하여 비교한다.&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;이런 자바스크립트의 null / undefined 문제와 비교해 , 코틀린과 자바는 어떻게 Null 안정성을 확보할까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;자바와 코틀린의 Null 처리&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;자바&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바의 경우, if-else 또는 Optional&amp;lt;T&amp;gt; 클래스를 사용해 Null을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional &amp;lt;T&amp;gt;의 의미는 &quot;&lt;b&gt;값이 있을 수도 있음&quot;을&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;&amp;nbsp;&lt;b&gt;if-else를 사용한 null 처리&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760844080450&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String result;
if (name != null) {
  result = name.toUpperCase();
} else {
  result = &quot;DEFAULT_USER&quot;;
}

System.out.println(result);&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;null이 될 수 있는 값을 Optional로 감싸서 null 처리&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760844129223&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Optional&amp;lt;String&amp;gt; nameOptional = getUsernameOptional();
String result = nameOptional // 값이 존재할 때만 함수 적용
.map(String::toUpperCase) // 값이 없으면 기본값을 반환
.orElse(&quot;DEFAULT_USER&quot;);

System.out.println(result);&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;Java의 경우, Optional &amp;lt;T&amp;gt;를 사용한 옵셔널 체이닝을 통해 null 체크 없는 코드를 작성할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;코틀린&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin의 경우엔 언어에서 null 안정성을 보장한다.&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;, 모든 타입은 기본적으로 null 값을 가질 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1760856725119&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    var name = &quot;&quot;
    name = null
    println(name)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행하면&lt;br /&gt;&lt;span style=&quot;background-color: #313336; color: #ec5424; text-align: start;&quot;&gt;Null cannot be a value of a non-null type 'String'&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Null을 할당할 수 없다는 에러가 나온다.&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;별도로 표기하지 않았을 경우고, 만약 null을 허용하고 싶다면 뒤에? 를 붙여주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1760856794150&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    var name: String? = &quot;&quot;
    name = null
    println(name)
}&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;이러면 null 이 출력된다.&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;위 언어적 특성을 사용해 kotlin에선 다음과 같은 방식으로 코드를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1760856894637&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    var name: String? = &quot;홍길동&quot;
    var length: Int? = name?.length // name이 null이 아니므로 3 할당

    println(length) // 출력: 3
    
    name = null
    length = name?.length          // name이 null이므로 전체 결과는 null 할당

    println(length) // 출력: null
}&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;자바스크립트에선 이런 문제를 null 체크를 통해 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 자바스크립트의 슈퍼셋인 타입스크립트에서 null을 어떻게 처리하는지 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;타입스크립트의 널 병합 연산자와 옵셔널 체이닝&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트는 &lt;b&gt;??(널 병합 연산자)&lt;/b&gt;&amp;nbsp;와 &lt;b&gt;?.(옵셔널 체이닝)&lt;/b&gt;&amp;nbsp; 두 가지 문법을 활용해 널 안정성을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(좀 더 정확하게 들어가면 '??'과 '?.'는 ECMAScript 2020 표준 문법으로 자바스크립트에서도 사용할 수 있다. 하지만 타입스크립트는 여기에 타입 안정성을 더해준다.)&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;b&gt;널 병합 연산자(??)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;널 병합 연산자(??)&lt;/b&gt;란 왼쪽 피연산자가 null 또는 undefined일 때 오른쪽 피연산자를 반환하고, 그렇지 않으면 왼쪽 피연산자를 반환하는 논리 연산자이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760860801953&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const foo = null ?? &quot;default string&quot;;
console.log(foo);
// 출력: &quot;default string&quot;

const baz = 0 ?? 42;
console.log(baz);
// 출력: 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 보면 || 연산자와 다른 점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;falsy 값이 와도 해당 값을 출력한다.&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;b&gt;옵셔널 체이닝(?.)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;?.&lt;/b&gt; 은 옵셔널 체이닝 연산자로, kotlin에서 null 처리하는 방식 그대로 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1760861489479&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const human = {
	name: &quot;홍길동&quot;,
	home: {
		city: &quot;서울&quot;,
		address: &quot;강남구&quot;,
	},
};

const zipcode = human.home.zipcode;
console.log(zipcode);

console.log(human.notExistsMethod?.());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행하면 undefined가 출력된다.&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;그래서 타입스크립트에선&lt;b&gt;.?&lt;/b&gt; 와&lt;b&gt;??&lt;/b&gt; 를 섞어서 null 처리를 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1760862261696&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface User {
	id: number;
	home?: {
		address?: {
			zipcode: string;
		} | null;
	};
}

const userWithoutHome: User = { id: 1};
const userWithHome: User = {id: 2, home: { address: {zipcode: &quot;A123&quot;}}};

const defaultZipcode = &quot;00000&quot;; // 기본값 정의

// 1. 집주소가 없는 사용자 처리
const zipcodeA = userWithoutHome.home?.address?.zipcode ?? defaultZipcode;

console.log(`집주소가 없는 사람의 우편번호 : ${zipcodeA}`); // 출력 00000

// 2. 집주소가 있는 사용자 처리
const zipcodeB = userWithHome.home?.address?.zipcode ?? defaultZipcode;

console.log(`집주소가 있는 사람의 우편번호 : ${zipcodeB}`);&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;pre id=&quot;code_1760862281530&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;집주소가 없는 사람의 우편번호 : 00000
집주소가 있는 사람의 우편번호 : A123&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;위와 같이 타입에러 또는 undefined, null 처리 없이 깔끔하게 처리할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;더 복잡한 상황이 온다면..?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 타입스크립트의 ?. 과 ?? 를 사용해 null을 안전하게 다루는 방법을 배웠다.&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;pre id=&quot;code_1760863547028&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function processUserData(userId: string) {
  const user = findUser(userId);
  if (!user) return null;
  
  const validated = validateUser(user);
  if (!validated) return null;
  
  const enriched = enrichUserData(validated);
  if (!enriched) return null;
  
  const formatted = formatForDisplay(enriched);
  if (!formatted) return null;
  
  return formatted;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 각 단계에서 실패할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로&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;[함수 호출 -&amp;gt; null 체크 -&amp;gt; 실패하면 null 반환 -&amp;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;/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;Kotlin&lt;/b&gt; 이었다면&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760863624411&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val result = findUser(userId)
    ?.let { validateUser(it) }
    ?.let { enrichUserData(it) }
    ?.let { formatForDisplay(it) }&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;let 체이닝을 사용해 처리했을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Java &lt;/b&gt;라면&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760863751842&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Optional&amp;lt;DisplayUser&amp;gt; result = findUser(userId)
    .flatMap(this::validateUser)
    .flatMap(this::enrichUserData)
    .map(this::formatForDisplay);&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;Optional 체이닝을 사용했을 것이다.&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;TypeScript에선 어떻게 할 수 있을까?&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;다음 글에선 Java의 Optional과 같은 타입을 직접 구현해 보자.&lt;/p&gt;</description>
      <category> ️ 내가 다시 볼 것</category>
      <author>전호영</author>
      <guid isPermaLink="true">https://securityinit.tistory.com/265</guid>
      <comments>https://securityinit.tistory.com/265#entry265comment</comments>
      <pubDate>Sun, 19 Oct 2025 17:51:19 +0900</pubDate>
    </item>
    <item>
      <title>스위치, L2, L3 에 대한 공부</title>
      <link>https://securityinit.tistory.com/264</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 서버를 다룰 때, 각 계층별 스위치에 대한 이야기를 많이 듣게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학교 학부 시절, 어렴풋이 배웠던 기억만 나지, 정확히 각 계층이 어떤 역할을 하고, 여러 스위치가 왜 존재하는지 등 자세히 설명할 수 없었기에, 실무에서 대화를 제대로 이어가기 어렵단 느낌을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 보니 이번에 해당 내용을 정리해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 실무를 하며 기본적으로 알아야 할 내용들에 대한 공부이므로, 교과서적인 정리는 아니고, 실무에서 이해할 정도만 정리해보려 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스위치&lt;/b&gt;&lt;/h2&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;스위치는 이름 그대로, 스위칭(Switching)을 해주는 도구이다.&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;자동차가 A -&amp;gt; D로 간다고 할 때, A는 D로 가는 길에 여러 갈림길을 만난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 D로 가기위해선 각 갈림길에서 D로 안내하는 이정표를 따라 이동할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 네트워크로 변경해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷(데이터)이 A -&amp;gt; D로 간다. 패킷도 목적지로 가며 여러 갈림길을 만난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 D로 가려면 각 네트워크 갈림길에서 D로 가는 경로를 따라 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 특정 목적지로 가는 경로를 따라 이동하는 것을 스위칭이라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(여기서 패킷은 L3 계층에서 데이터 단위이다.)&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;만약 이 근거가 IP라면, L3 스위칭을 한 것이고 ,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 근거가 MAC 주소라면 L2 스위칭을 한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스위치는 이런 근거들을 그냥 주먹구구 식으로 패킷에게 알려주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 목적지로 가기 위해선 어떤 경로를 선택해야 하는지 표로 정리하고, 이 표를 보고 각 패킷을 스위칭한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 각 스위치가 가지고 있는 표를 &lt;b&gt;라우팅 테이블(Routing Table)&lt;/b&gt;이라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(여기서도 당연히 L2 스위치라면 MAC 주소 테이블을 가지고 있다!)&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;그러므로 라우터는 L3 스위치의 일종이라 말할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(물론 통상적으로 스위치라 하면 L2 스위치를 의미한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데... 생각해 보면 같은 목적지로 가는데 한 방법만 있지는 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A -&amp;gt; B -&amp;gt; D로 갈 수 있고 또는 A -&amp;gt; C -&amp;gt; D 로 갈 수도 있다.&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;이때 이 선택의 기준이 되는 값을 매트릭(Metric)이라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 &lt;b&gt;&quot;값&quot;&lt;/b&gt;이자 &lt;b&gt;&quot;비용&quot;&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 매트릭 값이 낮을수록 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 매트릭 값이 낮은 방향으로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;L2 계층&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 by 회사겠지만... 우리 회사는 온프레미스 환경이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 인터넷 전용회선 및 보안장비를 교체하는 작업을 했는데, 서버실의 스위치와 각 층 및 사무실에 있는 스위치 역시 교체했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 각층에 존재하며 사무실에서 사용하는 스위치가 L2 스위치이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 L2가 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학교에서 배웠던 &lt;b&gt;L2는 &quot;데이터 링크 계층(Data Link Layer)&quot;로 동일한 네트워크 내 여러 장치 간의 데이터 전송을 담당하는 계층&lt;/b&gt;을 의미한다. 이때 &lt;b&gt;서로 주고받는 데이터의 전송 단위를 &quot;프레임(Frame)&quot;이라&lt;/b&gt; 하고, &lt;b&gt;MAC 주소를 기반으로 통신&lt;/b&gt;한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 동일한 네트워크 내 여러 장치란 쉽게 말해 우리 PC나 노트북을 의미한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 PC 또는 노트북에 랜선을 꽂고, 인터넷을 받고 소통을 한다. 여기서 랜선을 꽂아 인터넷 통신을 가능하게 하는 것을 &lt;b&gt;NIC(Network Interface Card) 또는 LAN(Local Area Network) 카드&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷이 라우터를 거쳐 L3 스위치, 그리고 L2 스위치를 통해 각 디바이스에 연결된다. 이때 이 디바이스의 유일한 H/W 주소를 MAC 주소라 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 서버실에 L3 스위치가 존재하고 각 사무실 또는 방에 L2 스위치가 존재하는 이유가 여기 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 우리 서버로 통신할 때 우리 서버의 IP를 찾아 통신이 들어온다. 이때 IP를 기준으로 들어온 외부 통신들을 적절한 내부 네트워크로 라우팅 해줘야 하기에, 서버실에 L3 스위치가 존재한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 IP를 기준으로, 어떤 IP에 해당하는 실제 H/W 장비에 요청을 전달해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 L2 스위치의 역할이 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L3 스위치는 목적지 IP 주소를 확인하고 해당 네트워크로 라우팅 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이때 IP에 해당하는 MAC 주소를 찾기 위해 ARP 프로토콜이 사용된다.)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 해당 네트워크에 연결된 각 층의 L2 스위치가 프레임을 받게 되고, L2 스위치는 프레임의 목적지 MAC 주소를 확인하여 자신의 MAC 테이블을 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 해당 MAC 주소를 가진 디바이스가 연결된 포트로 프레임을 스위칭한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUZ68J/btsQ2io9Y9X/ktqAnT12gU3TnqpFLIjgJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUZ68J/btsQ2io9Y9X/ktqAnT12gU3TnqpFLIjgJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUZ68J/btsQ2io9Y9X/ktqAnT12gU3TnqpFLIjgJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUZ68J%2FbtsQ2io9Y9X%2FktqAnT12gU3TnqpFLIjgJK%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;885&quot; height=&quot;631&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림처럼 동작을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 우리의 디바이스(엔드포인트)와 직접적으로 연결된 스위치를 L2 스위치라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;L3 계층&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 알고 있는 자유의 여신상은 아래 모습이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSuMpd/btsQ121rgib/AxO51iw3uPlKSR7qKfgBNk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSuMpd/btsQ121rgib/AxO51iw3uPlKSR7qKfgBNk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSuMpd/btsQ121rgib/AxO51iw3uPlKSR7qKfgBNk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSuMpd%2FbtsQ121rgib%2FAxO51iw3uPlKSR7qKfgBNk%2Fimg.jpg&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;330&quot; height=&quot;741&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 자유의 여신상은 프랑스에서 미국에 선물한 동상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 큰 동상을 어떻게 미국으로 옮겼을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmsCIy/btsQ4ohj0eu/9h1ASiVUFGWZaMKSI5OW9k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmsCIy/btsQ4ohj0eu/9h1ASiVUFGWZaMKSI5OW9k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmsCIy/btsQ4ohj0eu/9h1ASiVUFGWZaMKSI5OW9k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmsCIy%2FbtsQ4ohj0eu%2F9h1ASiVUFGWZaMKSI5OW9k%2Fimg.jpg&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;300&quot; height=&quot;207&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림처럼 자유의 여신상을 여러 조각내서 미국으로 옮기고, 미국에서 조립을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;우리가 인터넷에서 어떤 정보를 보낼 때, 이 정보를 통째로 보내지 않고 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;여러 개로 쪼개서 보낸 뒤, 수신 측에서 이를 조합한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;L3 계층(네트워크 계층)에서는 이렇게 전송되는 데이터 단위를 &lt;/span&gt;&lt;span&gt;&lt;b&gt;패킷(Packet)&lt;/b&gt;이라 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1800&quot; data-origin-height=&quot;781&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sQfgl/btsQ2fzdf9X/XD8jxFL1jrFPNfhdWcYSPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sQfgl/btsQ2fzdf9X/XD8jxFL1jrFPNfhdWcYSPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sQfgl/btsQ2fzdf9X/XD8jxFL1jrFPNfhdWcYSPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsQfgl%2FbtsQ2fzdf9X%2FXD8jxFL1jrFPNfhdWcYSPK%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;1800&quot; height=&quot;781&quot; data-origin-width=&quot;1800&quot; data-origin-height=&quot;781&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;위 표를 보면 Layer 3(네트워크 계층)에 PDU가 Packet이라 나와있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯 패킷은 L3 계층에서 사용하는 데이터 통신 단위이다.&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;L3 계층의 역할에 대한 설명을 보면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;멀티 노드 네트워크 환경을 구성하고 관리하며, 주소설정, 라우팅 그리고 트래픽 컨트롤을 담당한다.&quot;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&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&gt;L2 계층에선 MAC 주소로 각 노드를 식별했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그렇다면 L3에선 각 노드를 어떻게 식별할까?&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&gt;L3에서 사용되는 식별값이 &lt;b&gt;IP 주소&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;IP주소(여기선 IPv4를 기준으로 설명한다.)는 32비트로 구성되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 192.168.0.1 이란 값을 보면 4구간으로 나뉘어져 있고, 한 구간이 8비트값을 나타낸다.&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; 0~255&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;&amp;nbsp;L3와 IP 주소 등 이런 내용에 깊이 들어가기 전에, 스위치를 먼저 생각해보자.&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;위에 설명했듯이, 각 층과 각 방에 L2 스위치가 존재하고, 이 스위치는 개인 PC에 연결되어 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 서버실에 L3 스위치가 존재한다.&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;여기서 L3 스위치의 역할이 무엇이 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스위치의 역할이 특정 기준을 통해 스위칭을 하는 것이었고, L3 계층은 IP 주소를 전송 단위로 사용하니 IP 기준으로 스위칭(라우팅)을 하는 것이다.&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;L3 스위치의 주요 역할은 서로 다른 네트워크(VLAN) 간의 통신을 연결하는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 1층 네트워크와 2층 네트워크가 논리적으로 분리되어 있을 때, L3 스위치가 이들 사이의 라우팅을 처리한다.&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;IP는 인터넷에서 사용되는 주소인 것은 다 알고 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 외부에서 우리 사내 서버로 요청을 보내는 과정은 조금 더 복잡하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외부에서는 우리 회사의 공인 IP 주소로 요청을 보낸다.&lt;/b&gt; 이 요청은 먼저 &lt;b&gt;방화벽(또는 라우터)&lt;/b&gt;에 도착한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방화벽은 NAT(Network Address Translation)를 통해 공인 IP를 내부 사설 IP로 변환하고, 필요한 보안 검사를 수행한다.&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;L3 스위치가 등장&lt;/b&gt;한다. 만약 목적지 서버가 다른 VLAN에 있다면, L3 스위치가 해당 요청의 목적지 IP 주소를 확인하고 적절한 VLAN으로 라우팅한다. 같은 VLAN 내에서는 L2 스위치가 MAC 주소를 기반으로 최종 목적지까지 전달한다.&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;라우터를 따로 두는 경우가 있을 수 있고, L3 스위치를 라우터 역할까지 하도록 설정할 수도 있다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xLWdU/btsQ4o2KJ2e/vr5vK3PZpoxyxcxvmSwKx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xLWdU/btsQ4o2KJ2e/vr5vK3PZpoxyxcxvmSwKx1/img.png&quot; data-alt=&quot;내가 재직하는 회사의 인프라 구성을 참고했을 뿐, 이 네트워크 구성은 회사마다 달라진다!!!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xLWdU/btsQ4o2KJ2e/vr5vK3PZpoxyxcxvmSwKx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxLWdU%2FbtsQ4o2KJ2e%2Fvr5vK3PZpoxyxcxvmSwKx1%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;396&quot; height=&quot;746&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내가 재직하는 회사의 인프라 구성을 참고했을 뿐, 이 네트워크 구성은 회사마다 달라진다!!!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊게 들어가면 끝도 없으니 일단 네트워크 요청 흐름은 이정도로 설명을 마무리하고..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP에 대해 좀 더 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 주소를 보다보면 123.45.67.89/24 이런식으로 뒤에 24와 같은 숫자가 붙은 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 &lt;b&gt;CIDR(Classless Inter-Domain Routing)&lt;/b&gt;이라 부르고, 이 값은 IP의 Netwokr ID가 어디까지인지를 나타내는 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오잉?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Network ID란 무엇일까??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 주소는 두 부분으로 구성되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/orv7n/btsQ3ZPC36P/Kr5pRy9bsA6ZM22FsrH461/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/orv7n/btsQ3ZPC36P/Kr5pRy9bsA6ZM22FsrH461/img.jpg&quot; data-alt=&quot;https://blog.airsquirrels.com/what-is-an-ip-address-and-how-do-i-find-it&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/orv7n/btsQ3ZPC36P/Kr5pRy9bsA6ZM22FsrH461/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Forv7n%2FbtsQ3ZPC36P%2FKr5pRy9bsA6ZM22FsrH461%2Fimg.jpg&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;560&quot; height=&quot;260&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://blog.airsquirrels.com/what-is-an-ip-address-and-how-do-i-find-it&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Network ID란 이름 그대로 어느 네트워크에 속하는지 나타내는 주소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 서로 다른 두 IP의 Network ID가 같다면, 같은 네트워크에 있다는 뜻으로 직접 통신이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Network ID가 다르다면 서로 다른 네트워크에 있으므로 라우터를 통해 통신해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host ID란 하나의 호스트를 나타내는 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 네트워크 안에서 개별 디바이스를 구분하는 값이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(L2에선 MAC값으로 구분한다. L3에서 특정 디바이스를 나타내는 값은 전체 IP 주소이다.)&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;여기서 CIDR의 24 또는 16 이런 값들은 왼쪽부터 몇비트가 네트워크 ID인지를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/24라면 왼쪽부터 24비트까지가 네트워크 ID란 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;123.45.67.89/24 &amp;lt;- 이 경우엔 123.45.67 이 값이 네트워크 ID가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;CIDR 표기법과 서브넷 마스크는 같은 개념을 다르게 표현한 것일 뿐이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;과거에는 A, B, C 클래스로 고정 분할했지만(Classful), 현재는 CIDR을 통해 유연하게 네트워크 크기를 조정한다(Classless). &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;(서브넷 마스크와 클래스에 대해선 구글링을 해서 공부해보자!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ARP(Address Resolution Protocol)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 잠깐 나왔던 ARP 프로토콜에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L3가 IP 주소로 라우팅을 하고, 이 IP주소로 라우팅 된 패킷(데이터)은 L2 계층으로 내려가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 IP에 매핑된 실제 HW 장비(MAC 주소)를 어떻게 알 수 있을까?&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;이때 ARP이 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얘를 들어 PC1 -&amp;gt; PC2로 요청을 보낸다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PC1은 PC2의 IP주소(e.g. 192.168.10.6)은 알지만 MAC주소는 모르고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 ARP Request를 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 ARP Request는 Broadcast로, 같은 네트워크에 존재하는 모든 디바이스에 요청을 보낸다는 뜻이다.&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;ARP Request는 이런 느낌이다.&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;&quot;192.168.10.6에 해당하는 디바이스의 MAC값이 무엇인가요?! 알고 있으면 PC1에게 답해주세요!!&quot;&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;이때 네트워크에 있는 192.168.10.6은 하나만 존재할 것이다. 그러므로 해당 디바이스를 제외한 모든 디바이스는 응답을 하지 않고, PC2만 이 요청에 대해 응답한다.&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;&quot;제가 192.168.10.6이고, 제 MAC 주소는 AB:AB:AB:AB:AB:AB 입니다!&quot;&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;이렇게 ARP를 통해 알아낸 PC2의 MAC 주소를 PC1은 일정기간 캐싱한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무를 하고, 다른 엔지니어들과 대화하며 내가 잘 모르고 있다 느낀 부분들에 대해 정리해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크란 깊고 넓다 정말...&lt;/p&gt;</description>
      <category> ️ 내가 다시 볼 것</category>
      <author>전호영</author>
      <guid isPermaLink="true">https://securityinit.tistory.com/264</guid>
      <comments>https://securityinit.tistory.com/264#entry264comment</comments>
      <pubDate>Sat, 4 Oct 2025 23:56:00 +0900</pubDate>
    </item>
    <item>
      <title>Redis가 이벤트를 처리하는 과정을 탐구해보자.</title>
      <link>https://securityinit.tistory.com/263</link>
      <description>&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=5TRFpFBccQM&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/Lhmj1/hyZDVqlaxI/fm13sgLbpLMmxPSjjkDZL1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/bevKRy/hyZC66YqEW/leUNLBFc0kNStkPkxmRMv1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&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;System Design: Why is single-threaded Redis so fast?&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/5TRFpFBccQM&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 영상을 보며 Redis의 내부 Event Loop가 어떻게 동작하는지 궁금해져서 Redis 코드를 따라가 보기로 결심했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1756132898515&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 - redis/redis: For developers, who are building real-time data-driven applications, Redis is the preferred, fastest, and &quot; data-og-description=&quot;For developers, who are building real-time data-driven applications, Redis is the preferred, fastest, and most feature-rich cache, data structure server, and document and vector query engine. - red...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/redis/redis/tree/unstable&quot; data-og-url=&quot;https://github.com/redis/redis&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nAvWD/hyZzBN1OFJ/fYgkzflNQvKtL7try5QyzK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/BvViK/hyZCZ1ruz4/kDzkqS19eVXIsjkGmME0DK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/redis/redis/tree/unstable&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/redis/redis/tree/unstable&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nAvWD/hyZzBN1OFJ/fYgkzflNQvKtL7try5QyzK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/BvViK/hyZCZ1ruz4/kDzkqS19eVXIsjkGmME0DK/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 - redis/redis: For developers, who are building real-time data-driven applications, Redis is the preferred, fastest, and&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;For developers, who are building real-time data-driven applications, Redis is the preferred, fastest, and most feature-rich cache, data structure server, and document and vector query engine. - red...&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;redis-internal-arch.png&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmA6hU/btsP40aZlU0/mSpqw2JPlfFhaNBtP9cv60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmA6hU/btsP40aZlU0/mSpqw2JPlfFhaNBtP9cv60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmA6hU/btsP40aZlU0/mSpqw2JPlfFhaNBtP9cv60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmA6hU%2FbtsP40aZlU0%2FmSpqw2JPlfFhaNBtP9cv60%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;1137&quot; height=&quot;468&quot; data-filename=&quot;redis-internal-arch.png&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;468&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;영상에선 위 그림이 Redis가 이벤트를 처리하는 구조라고 나와있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 그런지 확인해 보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Redis가 이벤트를 처리하는 과정 큰 그림.&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 그림을 먼저 그려보면 Redis는 다음과 같이 이벤트를 처리한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트와 Redis 간의 연결 수립(TCP)&lt;/li&gt;
&lt;li&gt;Redis의 메인 스레드는 각 운영체제에 맞는&lt;b&gt; I/O 멀티플렉싱 시스템콜&lt;/b&gt;을 통해, &lt;b&gt;커널로부터 읽기 또는 쓰기 준비가 된 FD의 리스트&lt;/b&gt;를 받는다.&lt;b&gt;(I/O Multiplex 과정)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Redis의 EventLoop는 &lt;b&gt;전달받은 FD 리스트를 순차적으로 처리&lt;/b&gt;한다. &lt;b&gt;FD의 이벤트 유형&lt;/b&gt;에 따라, 적절한 &lt;b&gt;이벤트 핸들러 함수를 호출&lt;/b&gt;한다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;읽기(Read) 이벤트&lt;/b&gt; 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 해당 이벤트(FD)가 Read 상태라면, 해당 FD의 읽기 핸들러를 실행한다.(클라이언트의 명령어 - set, get etc - 를 파싱하고 실행)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;명령어 실행이 완료되고, 클라이언트에게 보낼 응답 데이터가 준비되면, 응답 데이터를 출력 버퍼에 저장한다. 그리고 해당 클라이언트(FD)를 쓰기 리스트에 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이벤트 루프가 한번 돌고 다음 이벤트 처리에 들어가기 전(&lt;b&gt;beforeSleep&lt;/b&gt; 단계)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대기 중인 FD들에게 응답 데이터 전송을 시도한다.&lt;/li&gt;
&lt;li&gt;전송에 실패하면 FD에 쓰기 이벤트 핸들러를 등록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쓰기(Write) 이벤트&lt;/b&gt; 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 해당 이벤트 FD가 Write 상태라면, 해당 FD의 쓰기 핸들러를 실행하고, 응답 데이터를 클라이언트에게 전달한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 클라이언트와의 연결을 제외한&lt;b&gt; 2, 3번이 Redis가 연결된 이벤트를 처리하는 과정&lt;/b&gt;이고, &lt;b&gt;Redis가 실행되고, 종료되기 직전까지 무한루프를 돌며 2,3번을 반복&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;여기서 무한루프를 돌면서 FD에 등록되어 있는 핸들러를 실행하는 Redis의 메인이벤트 처리 스레드는 1개만 존재한다. 이 메인 스레드가 모든 클라이언트 명령어를 순차적으로 처리하기 때문에 Redis를 싱글 스레드 모델이라고 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(참고: Redis 6.0부터는 네트워크 I/O를 처리하는 별도의 I/O 스레드들이 추가되었지만, 실제 명령어 실행과 데이터 처리는 여전히 단일 메인 스레드에서 처리한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아! 아직도 어렵다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FD는 뭐고, 이벤트란 뭔지... , &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;I/O 멀티플렉싱 등 여러 개념이 헷갈린다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;함께 공부해 가며 이해해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(이 글에선 클라이언트와 레디스 간의 연결 수립은 다루지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;연결이 수립되었다는 가정 하에, 이벤트를 어떻게 처리하는지 알아보자.)&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1.&amp;nbsp; IO Multiplex&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 공부하면 IO Multiplex라는 말을 많이 보게 되고, 이게 여러 클라이언트 요청(이벤트)을 동시에 처리하는데 큰 역할을 한다고 배운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IO Multiplex란 여러 개의 소켓(연결)에서 발생하는 이벤트를 하나의 시스템 콜로 감지하는 기술&lt;/b&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;그리고 각 클라이언트가 Redis에 이벤트를 발생시킨다. (여기서 이벤트란 특정 명령어(GET key, SET key value etc.)를 실행하는 것이라 보면 된다. 그 외에도 있는데, 해당 내용은 아래 이벤트루프를 설명할 때 나온다.)&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;즉, 시스템 콜이므로 각 OS에 따라 다르고, 개발자가 직접 개발하는 것이 아닌, 호출하는 것이다.&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;여러 소켓을 동시에 감시하고, 그중 I/O 작업(쓰기, 읽기)이 가능한 상태가 있다면, 해당 소켓과 발생한 이벤트 타입을 내부 배열(fired 배열에 저장)에 저장한다.&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;사실 소켓이란 네트워크 통신을 위한 추상적인 개념이다. 실제로 OS는 소켓을 다루기 위해 FD(File Discriptor)라는 정수값을 할당한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(클라이언트가 레디스와 연결이 되면, 각 클라이언트는 FD 값을 할당받는다. 그리고 Redis는 반복적으로 커널에 요청을 해서 기존 FD들 중에 이벤트가 준비된 것이 있다면, 해당 FD 값과 이벤트 타입을 Redis 내부 자료구조에 저장한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 I/O 작업 또는 이벤트를 다루는 기준이 FD 값이 되므로, 엄밀히 말하면 I/O 멀티플렉싱은 소켓에 할당된 FD를 감시하는 것이다.&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;이 소켓은 OS에서 FD(File Discriptor) 정수 값으로 표현된다.&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;그러므로 FD란 특정 소켓 연결을 가리키는 정수 인덱스이며, 하나의 클라이언트 연결당 하나의 FD가 할당된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(물론 새로운 연결이 들어오면, FD가 추가된다.)&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;이제부터 용어는 FD로 통일할 것이고, 이는 한 클라이언트와 서버 간의 연결을 의미하므로, 유의해서 읽으며 된다.&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;IO Multiplex 기술 덕분에 수많은 FD에서 발생하는 이벤트를 감지하고, 순차적으로 처리할 수 있게 된다.&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;/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;Redis 소스코드의 server.c의 int main() {} 함수를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이곳이 Redis의 진입점이다.&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;int main()의 마지막 즈음을 보면 다음과 같은 코드가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756024011424&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int main(int argc, char **argv) {
	...
	aeMain(server.el);
}&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;여기 aeMain이 이벤트 루프를 실행하는 함수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(server.el은 이벤트 루프 객체로, server.c의 initServer() 함수에서 aeCreateEventLoop()를 호출하여 생성한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756024069360&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ae.c
...
void aeMain(aeEventLoop *eventLoop) {
    eventLoop-&amp;gt;stop = 0;
    while (!eventLoop-&amp;gt;stop) {
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}
...&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;aeProcessEvents를 따라가 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756024451302&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ae.c
...

// 아래는 각 운영체제에 따라 aeApiPoll 시 불러올 메서드를 결정하는 로직
#ifdef HAVE_EVPORT
#include &quot;ae_evport.c&quot; // Solaris
#else
    #ifdef HAVE_EPOLL
    #include &quot;ae_epoll.c&quot; // 리눅스 
    #else
        #ifdef HAVE_KQUEUE
        #include &quot;ae_kqueue.c&quot; // 맥
        #else
        #include &quot;ae_select.c&quot; // 모든 플랫폼
        #endif
    #endif
#endif

...

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;
    
    if (eventLoop -&amp;gt; maxfd != -1 || ((flas &amp;amp; AE_TIME_EVENTS) &amp;amp;&amp;amp; !(flags &amp;amp; AE_DONT_WAIT))) {
    	...
        numevents = aeApiPoll(eventLoop, tvp);
        ...
    }
}&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;numevents = aeApiPoll(eventLoop, tvp);&lt;/b&gt; 가 IO Multiplex를 위한 코드이다.&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;aeApiPoll 함수로 따라가 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1756024789241&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ae_epoll.c
...
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop-&amp;gt;apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state-&amp;gt;epfd,state-&amp;gt;events,eventLoop-&amp;gt;setsize,
            tvp ? (tvp-&amp;gt;tv_sec*1000 + (tvp-&amp;gt;tv_usec + 999)/1000) : -1);
    if (retval &amp;gt; 0) {
        int j;

        numevents = retval;
        for (j = 0; j &amp;lt; numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state-&amp;gt;events+j;

            if (e-&amp;gt;events &amp;amp; EPOLLIN) mask |= AE_READABLE;
            if (e-&amp;gt;events &amp;amp; EPOLLOUT) mask |= AE_WRITABLE;
            if (e-&amp;gt;events &amp;amp; EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;
            if (e-&amp;gt;events &amp;amp; EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
            eventLoop-&amp;gt;fired[j].fd = e-&amp;gt;data.fd;
            eventLoop-&amp;gt;fired[j].mask = mask;
        }
    } else if (retval == -1 &amp;amp;&amp;amp; errno != EINTR) {
        panic(&quot;aeApiPoll: epoll_wait, %s&quot;, strerror(errno));
    }

    return numevents;
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(설명은 Linux를 기준으로 하기에, ae_epoll.c의 aeApiPoll()을 분석했다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756024871708&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;retval = epoll_wait(state-&amp;gt;epfd,state-&amp;gt;events,eventLoop-&amp;gt;setsize,
        tvp ? (tvp-&amp;gt;tv_sec*1000 + (tvp-&amp;gt;tv_usec + 999)/1000) : -1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 epoll_wait가 리눅스의 IO Mulitplex 시스템콜이다.&lt;/p&gt;
&lt;figure id=&quot;og_1756025172597&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;epoll_wait(2) - Linux manual page&quot; data-og-description=&quot;epoll_wait(2) &amp;mdash; Linux manual page epoll_wait(2) System Calls Manual epoll_wait(2) NAME &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; top epoll_wait, epoll_pwait, epoll_pwait2 - wait for an I/O event on an epoll file descriptor LIBRARY &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; top Standard C library (libc, -lc) SYNOPS&quot; data-og-host=&quot;man7.org&quot; data-og-source-url=&quot;https://man7.org/linux/man-pages/man2/epoll_wait.2.html&quot; data-og-url=&quot;https://man7.org/linux/man-pages/man2/epoll_wait.2.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man2/epoll_wait.2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://man7.org/linux/man-pages/man2/epoll_wait.2.html&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;epoll_wait(2) - Linux manual page&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;epoll_wait(2) &amp;mdash; Linux manual page epoll_wait(2) System Calls Manual epoll_wait(2) NAME &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; top epoll_wait, epoll_pwait, epoll_pwait2 - wait for an I/O event on an epoll file descriptor LIBRARY &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; top Standard C library (libc, -lc) SYNOPS&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;man7.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;epoll_wait()는 I/O 이벤트가 준비된 FD의 개수를 반환한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 현재 I/O 요청이 들어온(이벤트가 준비된) FD의 개수를 반환하는 것이다. (retval = epoll_wait())&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;여기서 Redis의 효율성이 드러난다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;A call to epoll_wait() will block until either: &lt;br /&gt;&amp;bull; a file descriptor delivers an event; &lt;br /&gt;&amp;bull; the call is interrupted by a signal handler; or &lt;br /&gt;&amp;bull; the timeout expires.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;epoll_wait()는 FD에 이벤트가 준비되었을 때&lt;/li&gt;
&lt;li&gt;외부 시그널에 의해 시스템 콜(epoll_wait()이 인터럽트 되었을 때 (예 : ctrl+c로 레디스 종료)&lt;/li&gt;
&lt;li&gt;타임아웃을 걸었을 경우, 해당 타임아웃이 만료되었을 때 (예: 직접 타임아웃 설정)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 3가지 경우에만 실행이 되고, 그 외엔 블로킹되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(즉, numevents = epoll_wait(); 에 멈춰있는 상태)&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;우리가 Redis에 특정 명령어를 보내 작업을 하지 않는다면, 아무것도 안 하고 있으므로, cpu 사용량이 없다.&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;다시 돌아가서, 아래 부분이 eventLoop가 처리할 fired 배열에 각 FD별 이벤트 타입을 등록하는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1756025339004&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ae_epoll.c

/* 지금 I/O 요청이 들어온 FD가 있다면 실행 */
if (retval &amp;gt; 0) {
    int j;

	/* 현재 I/O 요청이 들어온 FD 수 */
    numevents = retval; 
    for (j = 0; j &amp;lt; numevents; j++) {
        int mask = 0;
        /* j 번째 발생한 이벤트 정보 가져오기 */
        struct epoll_event *e = state-&amp;gt;events+j;

		/* 읽기 가능하면 읽기 플래그 설정 */
        if (e-&amp;gt;events &amp;amp; EPOLLIN) mask |= AE_READABLE;
        /* 쓰기 가능하면 쓰기 플래그 설정 */
        if (e-&amp;gt;events &amp;amp; EPOLLOUT) mask |= AE_WRITABLE;
        /* 에러 발생 시 읽기/쓰기 모두 플래그 설정 */
        if (e-&amp;gt;events &amp;amp; EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;
        /* 연결 끊김 시 읽기/쓰기 모두 플래그 설정 */
        if (e-&amp;gt;events &amp;amp; EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
        /* 발생한 이벤트의 파일 디스크립터 저장 */
        eventLoop-&amp;gt;fired[j].fd = e-&amp;gt;data.fd;
        /* 발생한 이벤트의 마스크 저장 */
        eventLoop-&amp;gt;fired[j].mask = mask;
    }
} else if (retval == -1 &amp;amp;&amp;amp; errno != EINTR) {
    panic(&quot;aeApiPoll: epoll_wait, %s&quot;, strerror(errno));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이벤트의 개수를 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1756025732933&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ae_epoll.c
...
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    ...
    if (retval &amp;gt; 0) {
    	...
    } else if (retval == -1 &amp;amp;&amp;amp; errno != EINTR) {
    	...
    }

    return numevents;
}
...&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;즉, eventLoop는 계속해서 aeApiPoll을 호출하면서, 현재 I/O 요청이 들어온 FD가 있는지 반복적으로 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 있다면 fired 배열에 저장된 FD와 이벤트를 순차적으로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정이 IO Multiplex를 통해 여러 이벤트를 이벤트루프로 전달하는 과정이다.&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;b&gt;2.&amp;nbsp; EventLoop의 이벤트 처리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;aeApiPoll()의 리턴값이 1 이상이라면, I/O 이벤트가 준비된 FD가 있다는 뜻이다.&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;FD는 2개의 이벤트 타입을 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 / 쓰기 이렇게 두 개의 이벤트 타입을 갖는데, 각각 다음 의미를 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라이언트 -&amp;gt; 레디스로 요청이 들어오는 경우는 Read Event&lt;/b&gt;이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레디스가 클라이언트의 요청을 처리하여 응답을 돌려주는 이벤트가 Write Event&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;/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;아래 aeProcessEvents()가 Redis의 이벤트 루프의 핵심으로, 여기서 이벤트를 처리한다.&lt;/p&gt;
&lt;pre id=&quot;code_1756080086382&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/// ae.c

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    if (eventLoop-&amp;gt;maxfd != -1 ||
        ((flags &amp;amp; AE_TIME_EVENTS) &amp;amp;&amp;amp; !(flags &amp;amp; AE_DONT_WAIT))) {

        ...

        numevents = aeApiPoll(eventLoop, tvp); // 준비된 이벤트의 개수를 반환

        ...

        /* 발생한 파일 이벤트들을 순차 처리 */
        for (j = 0; j &amp;lt; numevents; j++) {
            int fd = eventLoop-&amp;gt;fired[j].fd;
            aeFileEvent *fe = &amp;amp;eventLoop-&amp;gt;events[fd];
            int mask = eventLoop-&amp;gt;fired[j].mask;
             /* 현재 fd에서 실행된 이벤트 개수 */
            int fired = 0;

			...
            
            /* 이벤트가 여전히 유효한지 &quot;fe-&amp;gt;mask &amp;amp; mask &amp;amp; ...&quot; 로 확인
             * 순서가 뒤바뀌지 않으면 읽기 이벤트 먼저 실행 */
            if (!invert &amp;amp;&amp;amp; fe-&amp;gt;mask &amp;amp; mask &amp;amp; AE_READABLE) {
            	/* 읽기 이벤트 핸들러 함수(콜백함수) 실행 (예: 클라이언트 명령어 실행) */
                fe-&amp;gt;rfileProc(eventLoop,fd,fe-&amp;gt;clientData,mask);
                fired++;
                fe = &amp;amp;eventLoop-&amp;gt;events[fd]; // 콜백에서 배열 크기가 변경될 수 있으므로 포인터 갱신
            }

            /* 쓰기 이벤트 실행 */
            if (fe-&amp;gt;mask &amp;amp; mask &amp;amp; AE_WRITABLE) {
                /* 같은 콜백 함수가 읽기와 쓰기에 모두 등록된 경우 중복 실행 방지 */
                if (!fired || fe-&amp;gt;wfileProc != fe-&amp;gt;rfileProc) {
                    /* 쓰기 이벤트 콜백 실행 (예: 클라이언트 응답 쓰기) */
                    fe-&amp;gt;wfileProc(eventLoop,fd,fe-&amp;gt;clientData,mask);
                    fired++;
                }
            }

           ...

            /* 처리된 파일 이벤트 개수 증가 */
            processed++;
        }
    }
    ...

    /* 처리된 총 이벤트 개수 반환 (파일 이벤트 + 타이머 이벤트) */
    return processed;
}&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;처리할 이벤트가 있다면, 처리할 이벤트 개수(numevents)만큼 루프를 돌면서 이벤트를 처리한다.&lt;/p&gt;
&lt;pre id=&quot;code_1756088720777&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* numevents == 현재 이벤트가 준비된 FD의 개수
   발생한 파일 이벤트들을 순차 처리 */
for (j = 0; j &amp;lt; numevents; j++) {
    /* 처리할 파일 디스크립터(클라이언트를 의미) */
    int fd = eventLoop-&amp;gt;fired[j].fd; 
    /* 해당 fd에 대한 파일 이벤트 구조체 */
    aeFileEvent *fe = &amp;amp;eventLoop-&amp;gt;events[fd];
    /* 발생한 이벤트 마스크(어떤 이벤트가 발생했는지) */
    int mask = eventLoop-&amp;gt;fired[j].mask;
	...
}&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;그 후 해당 fd에 어떤 이벤트가 준비되어 있는지, 확인하고 이벤트를 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 aeFileEvent 구조체로,&amp;nbsp;FD와 연결된 I/O 이벤트를 관리하기 위한 구조체이다.&lt;/p&gt;
&lt;pre id=&quot;code_1756096944600&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;typedef struct aeFileEvent {
    /* 이벤트 마스크: AE_READABLE|AE_WRITABLE|AE_BARRIER 중 하나 또는 조합
    * 어떤 종류의 I/O 이벤트를 감지할지 결정 */
    int mask;

    /* 읽기 가능 이벤트 발생 시 호출될 콜백 함수 */
    aeFileProc *rfileProc; 

    /* 쓰기 가능 이벤트 발생 시 호출될 콜백 함수
    * 클라이언트 응답 전송, 버퍼링된 데이터 출력 등에 사용 */
    aeFileProc *wfileProc; 
    
    /* 콜백 함수에 전달될 사용자 정의 데이터
    * 클라이언트 연결 정보 etc */
    void *clientData; 
} aeFileEvent;&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;아래 코드를 보면 fe(파일 이벤트)가 읽기 이벤트를 달고 있다면, 읽기 이벤트 핸들러 함수를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(invert 플래그가 있지만, 이벤트 처리에 중요한 로직은 아니므로 넘어가자.)&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;만약 fe에 달린 이벤트가 쓰기 이벤트라면 , 쓰기 이벤트 핸들러 함수를 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1756097001488&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* 읽기 이벤트 실행
 * AE_READABLE */
if (!invert &amp;amp;&amp;amp; fe-&amp;gt;mask &amp;amp; mask &amp;amp; AE_READABLE) {
    /* 읽기 이벤트 핸들러 함수(콜백함수) 실행 (예: 클라이언트 명령어 실행) */
    fe-&amp;gt;rfileProc(eventLoop,fd,fe-&amp;gt;clientData,mask);
    fired++;
    /* 콜백에서 배열 크기가 변경될 수 있으므로 포인터 갱신 */
    fe = &amp;amp;eventLoop-&amp;gt;events[fd];
}

/* 쓰기 이벤트 실행 */
 * AE_WRITABLE */
if (fe-&amp;gt;mask &amp;amp; mask &amp;amp; AE_WRITABLE) {
    /* 같은 콜백 함수가 읽기와 쓰기에 모두 등록된 경우 중복 실행 방지 */
    if (!fired || fe-&amp;gt;wfileProc != fe-&amp;gt;rfileProc) {
        /* 쓰기 이벤트 콜백 실행 (예: 클라이언트 응답 쓰기) */
        fe-&amp;gt;wfileProc(eventLoop,fd,fe-&amp;gt;clientData,mask);
        fired++;
    }
}&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;이렇게 이벤트를 처리하는 것이 Redis EventLoop의 핵심 로직이다.&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;이렇게 한 번의 루프를 돌고 다음 이벤트 처리 루프로 돌아왔을 때의 beforesleep을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(beforesleep은 많은 역할을 하지만, 현재 내가 궁금한 것은 어떻게 클라이언트에게 응답을 전송하는지이므로, 해당 부분만 살펴보자)&lt;/p&gt;
&lt;pre id=&quot;code_1756129507592&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ae.c

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    if (eventLoop-&amp;gt;maxfd != -1 ||
        ((flags &amp;amp; AE_TIME_EVENTS) &amp;amp;&amp;amp; !(flags &amp;amp; AE_DONT_WAIT))) {
        int j;
		...
        /* 이벤트 대기 전 befroesleep 실행 */
        if (eventLoop-&amp;gt;beforesleep != NULL &amp;amp;&amp;amp; (flags &amp;amp; AE_CALL_BEFORE_SLEEP))
            eventLoop-&amp;gt;beforesleep(eventLoop);
	...
}&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;beforesleep()의 구현체는 server.c에 존재하므로, 따라가 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1756129933023&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// server.c

void beforeSleep(struct aeEventLoop *eventLoop) {
    ...
    if (ProcessingEventsWhileBlocked) {
        ...
        /* 클라이언트에게 쓰기 시도 */
        processed += handleClientsWithPendingWrites();
        ...
        return;
    }
    ...
    handleClientsWithPendingWrites();
	...
}&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;handleClientsWithPendingWrites() 함수가 클라이언트에게 응답을 전송하는 부분이므로, 다시 따라가자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756131494138&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// networking.c

int handleClientsWithPendingWrites(void) {
    ...
    while((ln = listNext(&amp;amp;li))) {
        ...
        /* Try to write buffers to the client socket. */
        if (writeToClient(c,0) == C_ERR) continue;
        
        /* If after the synchronous writes above we still have data to
         * output to the client, we need to install the writable handler. */
        if (clientHasPendingReplies(c)) {
            installClientWriteHandler(c);
        }
    }
    return processed;
}&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;Redis 코드 주석에도 나와있듯이, writeToClient가 클라이언트 소켓(FD)에 응답 버퍼를 적는 부분이라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, FD에 데이터를 전송하는 부분이다.&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;그리고 쓰기 작업을 시도한 후에도, FD에 보낼 데이터가 남아있다면, 쓰기 이벤트 핸들러를 해당 FD에 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1756131667501&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* If after the synchronous writes above we still have data to
 * output to the client, we need to install the writable handler. */
if (clientHasPendingReplies(c)) {
    installClientWriteHandler(c);
}&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;마지막으로 writeToClient()로 들어가 어떻게 전송을 하는지 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756131982232&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// networking.c

int writeToClient(client *c, int handler_installed) {
    ...
    ssize_t nwritten = 0, totwritten = 0;  /* 이번 호출에서 쓴 바이트 수 */
    /* 슬레이브 여부 */
    const int is_slave = clientTypeIsSlave(c);

    /* 슬레이브(복제본) 클라이언트 처리 */
    if (unlikely(is_slave)) {
        /* 슬레이브인지 아닌지 여부에 따라 다르게 처리 */
        while(_clientHasPendingRepliesSlave(c)) {
            int ret = _writeToClientSlave(c, &amp;amp;nwritten);
            if (ret == C_ERR) break;
            totwritten += nwritten;
        }
        atomicIncr(server.stat_net_repl_output_bytes, totwritten);
    } else {
        /* 일반 클라이언트 처리 */
        const int is_normal_client = !(c-&amp;gt;flags &amp;amp; CLIENT_SLAVE);
        
        while (_clientHasPendingRepliesNonSlave(c)) {
            int ret = _writeToClientNonSlave(c, &amp;amp;nwritten);
            if (ret == C_ERR) break;
            totwritten += nwritten;
            ...
        }
        atomicIncr(server.stat_net_output_bytes, totwritten);
    }
    ...
    return C_OK;
}&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;클라이언트가 slave인지 여부에 따라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_writeToClientSlave()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_writeToClientNonSlave()&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나눠서 응답을 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;pre id=&quot;code_1756132125702&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;*nwritten = connWrite(c-&amp;gt;conn, c-&amp;gt;buf + c-&amp;gt;sentlen, c-&amp;gt;bufpos - c-&amp;gt;sentlen);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1756132151173&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static inline int connWrite(connection *conn, const void *data, size_t data_len) {
    return conn-&amp;gt;type-&amp;gt;write(conn, data, data_len);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection type(tcp 연결, tls 연결 등)에 따라 다르게 write가 달려있으므로, type에 맞게 write를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756132186650&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// connection.h

/* IO */
int (*write)(struct connection *conn, const void *data, size_t data_len);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection에 data 길이만큼 data를 보낸다는 뜻이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 위 beforesleep() 이 끝난 후에야 Redis의 메인스레드가 epoll_wait()을 실행하고, epoll_wait()가 동작하는 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;이렇게 길었던 EventLoop의 동작 방식에 대해 알아보았다.&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;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;redis-internal-arch.png&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmA6hU/btsP40aZlU0/mSpqw2JPlfFhaNBtP9cv60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmA6hU/btsP40aZlU0/mSpqw2JPlfFhaNBtP9cv60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmA6hU/btsP40aZlU0/mSpqw2JPlfFhaNBtP9cv60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmA6hU%2FbtsP40aZlU0%2FmSpqw2JPlfFhaNBtP9cv60%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;1137&quot; height=&quot;468&quot; data-filename=&quot;redis-internal-arch.png&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;468&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDEw0o/btsP4eBIKb9/nnK9JyzsYFSWKIVXsyhVxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDEw0o/btsP4eBIKb9/nnK9JyzsYFSWKIVXsyhVxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDEw0o/btsP4eBIKb9/nnK9JyzsYFSWKIVXsyhVxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDEw0o%2FbtsP4eBIKb9%2FnnK9JyzsYFSWKIVXsyhVxK%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;792&quot; height=&quot;450&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분만 살펴보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 코드를 전부 따라가 본 결과, Task Queue -&amp;gt; Event Dispatcher -&amp;gt; Event Processors 과정과 Redis가 이벤트를 처리하는 과정은 조금 다르다.&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;Redis는 Task Queue가 아닌 &lt;b&gt;이벤트가 준비된 FD 배열을 받고, 해당 배열을 순차적으로 순회하며 각 이벤트 타입에 따라 이벤트 핸들러를 실행한다.&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;위 그림은 일반적인 이벤트 기반 아키텍처에 더 가깝고, Redis는 뒷 단계를 하나의 EventLoop에서 처리한다고 보면 된다.&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;&amp;nbsp;&lt;/p&gt;</description>
      <category> ️ 내가 다시 볼 것</category>
      <author>전호영</author>
      <guid isPermaLink="true">https://securityinit.tistory.com/263</guid>
      <comments>https://securityinit.tistory.com/263#entry263comment</comments>
      <pubDate>Mon, 25 Aug 2025 23:42:25 +0900</pubDate>
    </item>
    <item>
      <title>Turborepo, NestJS, React, Pnpm 으로 구성하는 Monorepo</title>
      <link>https://securityinit.tistory.com/262</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 Java SpringBoot를 백엔드로, html, css, js를 프론트엔드로 구성한 서비스를 마이그레이션 하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀스택으로 개발을 해야했고, 빠르게 프로토타입을 만드는 것이 목표였기에, TS 기반의 기술을 사용하여 빠르게 개발하고, 같은 언어가 주는 장점인 타입 안정성 유지하려 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 모노레포를 사용하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 모노레포인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 모노레포를 선택한 이유 크게 다음 5가지이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 타입 공유의 완전성&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@koita/shared 패키지에 정의된 타입(예: IAttend, AttendStatus)을 백엔드와 프론트엔드가 공유한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를통해 API의 응답 데이터나 상태 관리 시 일관된 타입 시스템 유지가 가능해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// @koita/shared에서 정의한 타입을
// 백엔드와 프론트엔드가 동시에 사용
import { AttendStatus, IAttend } from '@koita/shared';

// 백엔드 (NestJS)
async createAttend(data: IAttend) { ... }

// 프론트엔드 (React)
const [status, setStatus] = useState&amp;lt;AttendStatus&amp;gt;(AttendStatus.NONE);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 동기화된 개발과 타입 안정성&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;API 스키마 변경&lt;/b&gt; 시, 공유 패키지의 타입만 수정하면 프론트엔드에서 즉시 에러를 확인할 수 있어 API 문서화나 수동 동기화 과정이 필요 없없다.&lt;/li&gt;
&lt;li&gt;enum 같은 공통 상수를 추가하거나 수정할 때도 모든 프로젝트에 일관성이 보장되어 &lt;b&gt;타입 안정성&lt;/b&gt;이 향상된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 코드 재사용성 향상&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;유틸리티 함수 공유:&lt;/b&gt; 날짜 포맷팅, 유효성 검사 등 공통 로직을 별도의 utils 패키지로 분리하여 코드 중복을 최소화할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 쉬운 리팩토링 및 유지보수&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;즉각적인 영향 파악:&lt;/b&gt; 공유 패키지의 코드를 변경하면, 이를 사용하는 모든 프로젝트에서 &lt;b&gt;즉시 에러를 감지&lt;/b&gt;할 수 있어 예상치 못한 버그방지가 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;통합된 코드 베이스:&lt;/b&gt; 모든 코드가 한곳에 있어 특정 기능을 개선하거나 수정할 때 관련 코드를 한눈에 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 효율적인 개발 환경 구축&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;통합된 의존성 관리:&lt;/b&gt; pnpm install 한 번으로 모든 패키지의 의존성을 한 번에 설치하고 관리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관된 개발자 경험 (DX):&lt;/b&gt; 모든 개발자가 동일한 버전의 의존성과 개발 도구 설정을 사용하게 되어 '내 컴퓨터에서는 잘 되는데...'와 같은 문제를 해결할 수 있다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 &lt;b&gt;개발의 효율성과 코드의 안정성&lt;/b&gt;을 동시에 확보할 수 있을 것이라 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 모노레포의 장점을 누리기 위해, 모노레포 구성을 도와주는 터보레포를 사용하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 모노레포가 처음이었기에 혼자 구성하기엔 무리가 있었다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모노레포에 대해 더 자세히 알고 싶다면 아래 글을 읽어보길 추천한다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1754957621400&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;Monorepo Explained&quot; data-og-description=&quot;Everything you need to know about monorepos, and the tools to build them.&quot; data-og-host=&quot;monorepo.tools&quot; data-og-source-url=&quot;https://monorepo.tools/&quot; data-og-url=&quot;https://monorepo.tools/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KA2hz/hyZzE2Q5x7/u9R12p6pbc231U9AEGwcs1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/QMhbz/hyZyrW0azF/7SEJhgQCPObJC27E4anuD1/img.jpg?width=1974&amp;amp;height=1481&amp;amp;face=0_0_1974_1481&quot;&gt;&lt;a href=&quot;https://monorepo.tools/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://monorepo.tools/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KA2hz/hyZzE2Q5x7/u9R12p6pbc231U9AEGwcs1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/QMhbz/hyZyrW0azF/7SEJhgQCPObJC27E4anuD1/img.jpg?width=1974&amp;amp;height=1481&amp;amp;face=0_0_1974_1481');&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;Monorepo Explained&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Everything you need to know about monorepos, and the tools to build them.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;monorepo.tools&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;터보레포란?&lt;/b&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1754533467887&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;Turborepo&quot; data-og-description=&quot;Turborepo is a build system optimized for JavaScript and TypeScript, written in Rust.&quot; data-og-host=&quot;turborepo.com&quot; data-og-source-url=&quot;https://turborepo.com/&quot; data-og-url=&quot;https://turborepo.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cwegLc/hyZuJ47153/gD9dcjWTEooj1fS5CefLwK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/8J1km/hyZuvFPzhS/UHtRrKqauIDW9KiQdK8OtK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://turborepo.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://turborepo.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cwegLc/hyZuJ47153/gD9dcjWTEooj1fS5CefLwK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/8J1km/hyZuvFPzhS/UHtRrKqauIDW9KiQdK8OtK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;Turborepo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Turborepo is a build system optimized for JavaScript and TypeScript, written in Rust.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;turborepo.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;blockquote data-ke-style=&quot;style2&quot;&gt;Turborepo is a high-performance build system for JavaScript and TypeScript codebases. It is designed for scaling monorepos and also makes workflows in single-package workspaces faster, too.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터보레포 공식문서에 나와 있는 내용이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS, TS 기반의 코드 베이스 빌드 시스템으로, 모노레포의 확장을 위해 만들어졌다고 한다.&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;b&gt;그래서 모노레포가 가지는 문제가 뭘까?&lt;/b&gt;&lt;/h3&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;코드베이스가 커지면 빌드, 테스트, Git 작업 등의 속도가 느려진다.&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;이를 해결하기 위해 터보레포는 내가 한 모든 작업을 로컬 캐시 스토어에 저장해, CI 시 같은 작업을 2번 반복하지 않도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업내용(빌드, 테스트, 의존성 등) 에 대한 고유 해시를 만들고 이를 캐시 스토어에 저장한다. 이 해시를 통해 작업이 실행되었는지 여부를 판단하고, 해시값이 동일하다면 스토어에서 작업 내용을 가져와 반복 작업을 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 여러 작업을 병렬적으로 실행하면서 CI 작업 속도를 높인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 장점은 손 쉽고, 빠르게 어떤 레포지토리든 turbo.json 만 추가한다면 터보레포를 사용할 수 있다는 점이다.(어떤 패키지 매니저를 사용해도상관 없다!)&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;터보레포는 모노레포 환경에서 캐싱과 병렬 처리를 통해 CI에 발생하는 반복작업을 줄여 개발 속도를 높이는 빌드 시스템이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;모노레포를 구성해보자.&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성하기 전, 폴더 구조에 대해 정의해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1754959538037&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  summer/                          # 루트
  ├── package.json                 # 워크스페이스 루트 설정
  ├── pnpm-workspace.yaml          # pnpm 워크스페이스 정의
  ├── turbo.json                   # Turbo 빌드 파이프라인 설정
  ├── pnpm-lock.yaml              
  ├── apps/                       # 애플리케이션들
  │   ├── backend/                # NestJS 백엔드
  │   │
  │   └── frontend/               # React + Vite 프론트엔드
  │
  └── packages/                   # 공유 패키지&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;TS 풀스택 프로젝트의 타입 안정성을 유지하기 위해 공통으로 사용되는 타입이나 인터페이스는 packages에 묶고, 백엔드와 프런트엔드는 apps라는 워크스페이스에 각각 구성했다&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;b&gt;워크스페이스가 뭘까?&lt;/b&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 워크스페이스는 자체적으로 package.json을 가지고 있기 때문에, 프론트엔드, 백엔드 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;이러한 구조 덕분에 워크스페이스 간 의존성 관리가 훨씬 편해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;(만약 따로 관리한다고 생각해 보자. 만약 백,프론트가 공통으로 사용하는 타입이 있다면 이 공통 코드를 npm 패키지로 배포하고, 이를 내려받아 쓰는 등 비효율이 발생한다. 공통 코드가 바뀌면 백 , 프론트 두 저장소 모두 수동으로 수정해야 하는 등 비효율적이다.)&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;예를 들어, 웹 애플리케이션 워크스페이스가 공통 컴포넌트 워크스페이스를 의존할 때, 패키지 매니저(npm, yarn, pnpm)가 이 두 워크스페이스를 자동으로 연결해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;(패키지 매니저가 모노레포 루트에 있는 설정 파일(package.json , pnpm-workspace.yaml) 을 보고 , 워크스페이스 간 의존 관계를 파악해 서로 참조할 수 있도록 해준다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 굳이 패키지를 따로 배포할 필요 없이 코드를 쉽게 가져다 쓸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 모노레포 안에서 코드 공유를 쉽게 해주고 모노레포 속 프로젝트별 독립성을 유지할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;모노레포 구성 순서&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 물리적인 폴더 구조를 생성한다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1754969760574&quot; class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;  summer/                          # 루트
  ├── package.json                 # 워크스페이스 루트 설정
  ├── pnpm-workspace.yaml          # pnpm 워크스페이스 정의
  ├── turbo.json                   # Turbo 빌드 파이프라인 설정
  ├── pnpm-lock.yaml              
  ├── apps/                       # 애플리케이션들
  │   ├── backend/                # NestJS 백엔드
  │   │
  │   └── frontend/               # React + Vite 프론트엔드
  │
  └── packages/                   # 공유 패키지             
      │
      └── shared/&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;나의 경우 apps 안에 backend, frontend 가 들어가고, 모든 곳에서 사용할 공유 패키지를 packages 폴더에 넣기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm을 패키지 매니저로 사용할 예정이므로,&lt;/p&gt;
&lt;pre id=&quot;code_1754972007131&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pnpm dlx create-turbo@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 명령어를 통해 기본 구조를 설정한다.&amp;nbsp;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 후 삭제할 폴더들을 삭제하고, 내가 구현하려는 구조에 맞게 수정한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. root 에서 관리할 의존성 및 스크립트를 package.json과 turbo.json에 작성한다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm init 을 통해 기본 package.json 을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공용으로 사용할 turbo, prettier, eslint 등을 pnpm을 통해 설치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 각자 설정에 맞게 node, pnpm 버전, pnpm의 esbuild 버전을 고정하는 등 개인 환경에 맞게 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 최소 node, pnpm 버전, esbuild의 의존성 충돌을 해결하기 위해 특정 버전 및 값을 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754970219847&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
	...
  &quot;engines&quot;: {
    &quot;node&quot;: &quot;&amp;gt;=18.0.0&quot;,
    &quot;pnpm&quot;: &quot;&amp;gt;=8.0.0&quot;
  },
  &quot;packageManager&quot;: &quot;pnpm@8.12.1&quot;,
  &quot;pnpm&quot;: {
    &quot;overrides&quot;: {
      &quot;esbuild&quot;: &quot;^0.25.8&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터보레포를 사용해 모노레포를 관리할 예정이므로 turbo.json에 설정을 추가한다.&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;그러므로 모노레포에서 실행해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터보레포는 pnpm-worksapce.yaml 파일이 존재하는 위치를 루트로 판단하기에&amp;nbsp; 먼저 레포지토리 루트에 pnpm-workspace.yaml 을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( pnpm dlx create-turbo@version 스크립트로 초기 구성을 했다면 이미 pnpm-workspace.yaml 파일이 생성되었을 것이다. 그러므로 아래 단계는 생략할 수 있다.)&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;우리의 경우 apps와 packages 가 독립적인 워크스페이스로 한 레포지토리 안에 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 워크스페이스를 pnpm-workspace.yaml에 적어주자.&lt;/p&gt;
&lt;pre id=&quot;code_1754970962427&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;packages:
  - 'apps/*'
  - 'packages/*'&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;이제 turbo.json을 적으면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 역시 root에 위치해야하고, 스크립트를 통해 생성한 프로젝트라면 기본으로 turbo.json이 존재한다.)&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;turbo.json&lt;/b&gt;&lt;span&gt; 은 Truborepo 의 태스크 파이프라인을 정의하는 설정파일이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;turbo.json은 아래와 같은 역할을 한다.&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;&amp;nbsp;1.&amp;nbsp;태스크&amp;nbsp;의존성&amp;nbsp;관리:&amp;nbsp;어떤&amp;nbsp;태스크가&amp;nbsp;먼저&amp;nbsp;실행되어야&amp;nbsp;하는지&amp;nbsp;정의&lt;br /&gt;&amp;nbsp;&amp;nbsp;2.&amp;nbsp;캐싱&amp;nbsp;최적화:&amp;nbsp;변경되지&amp;nbsp;않은&amp;nbsp;태스크의&amp;nbsp;결과를&amp;nbsp;재사용&lt;br /&gt;&amp;nbsp;&amp;nbsp;3.&amp;nbsp;병렬&amp;nbsp;실행:&amp;nbsp;의존성이&amp;nbsp;없는&amp;nbsp;태스크들을&amp;nbsp;동시에&amp;nbsp;실행&lt;br /&gt;&amp;nbsp;&amp;nbsp;4.&amp;nbsp;입력/출력&amp;nbsp;정의:&amp;nbsp;캐싱&amp;nbsp;대상&amp;nbsp;파일들을&amp;nbsp;명시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;turbo.json에 task로 설정한 이름 (build, dev, lint etc) 은 실제 각 프로젝트(나의 경우 frontend, backend, shared) 의 명령어와 같아야 한다.&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;turbo run build 명령어를 실행하면 , 각 레포의 package.json에 들어가 build 명령어를 찾아서 실행해준다.&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;내가 설정한 turbo.json 을 분석해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 이번 프로젝트에선 Turbo v1을 사용했기에 tasks 명령어가 아닌 pipeline 명령어를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Turbo v2를 사용한다면, pipeline을 tasks로 바꿔야 한다.)&lt;/p&gt;
&lt;pre id=&quot;code_1755156651707&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;pipeline&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [
        &quot;^build&quot;
      ],
      &quot;outputs&quot;: [
        &quot;dist/**&quot;,
      ]
    },
    &quot;dev&quot;: {
      &quot;cache&quot;: false,
      &quot;persistent&quot;: true
    },
    &quot;type-check&quot;: {
      &quot;dependsOn&quot;: [
        &quot;^build&quot;
      ]
    },
    &quot;lint&quot;: {
      &quot;dependsOn&quot;: [
        &quot;^build&quot;
      ]
    },
    &quot;test&quot;: {
      &quot;dependsOn&quot;: [
        &quot;^build&quot;
      ]
    },
    &quot;clean&quot;: {
      &quot;cache&quot;: false
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 요소에 대해 살펴보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;pre id=&quot;code_1755246746951&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;build&quot;: {
  &quot;dependsOn&quot;: [
    &quot;^build&quot;
  ],
  &quot;outputs&quot;: [
    &quot;dist/**&quot;,
  ]
},&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&lt;/p&gt;
&lt;pre id=&quot;code_1755247045797&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;dependsOn&quot; : [&quot;^build&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;여기 ^는 터보레포에게 의존성 그래프를 분석해 현재 패키지의 의존성 패키지들을 먼저 build하고, 현재 패키지를 build 하란 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 패키지가 B 패키지에 의존하고 있다면 , A를 빌드하기 전, B를 먼저 빌드하고 A를 빌드하게 된다.&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;우리의 경우, frontend, backend 모두 packages의 shared에 의존하고 있다.&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;backend/package.json&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755247337510&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
   ...
  &quot;scripts&quot;: {
  	...
  },
  &quot;dependencies&quot;: {
    &quot;@koita/shared&quot;: &quot;workspace:*&quot;,
    ...
  },
  &quot;devDependencies&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;frontend/package.json&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755247397479&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  ...
  &quot;scripts&quot;: {
  	...
  },
  &quot;dependencies&quot;: {
    &quot;@koita/shared&quot;: &quot;workspace:*&quot;,
    ...
  },
  &quot;devDependencies&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;shared/package.json&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755247450712&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;@koita/shared&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;Shared types and utilities for KOITA project&quot;,
  &quot;main&quot;: &quot;dist/cjs/index.js&quot;,
  &quot;module&quot;: &quot;dist/esm/index.js&quot;,
  &quot;types&quot;: &quot;dist/types/index.d.ts&quot;,
  &quot;exports&quot;: {
    &quot;.&quot;: {
    ...
    }
  },
  &quot;scripts&quot;: {
  	...
  },
  &quot;dependencies&quot;: {
    &quot;class-validator&quot;: &quot;^0.14.0&quot;,
    &quot;class-transformer&quot;: &quot;^0.5.1&quot;
  },
  &quot;devDependencies&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; turbo run build를 실행하면 frontend, backend 모두 shared에 의존하고 있으므로 shared를 먼저 build하고, frontend 와 backend를 build 하게 된다.&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;그리고 해당 build 결과물을 dist/ 폴더에 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755247722920&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;dev&quot;: {
  &quot;cache&quot;: false,
  &quot;persistent&quot;: true
},&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정은 dev 명령어(turbo run dev)를 실행 시 dev 작업 결과는 캐싱하지 않으며, persistent : true 설정 시 , 해당 dev 작업이 종료되지 않는 장기 실행 프로세스임을 알려준다. 이를 통해 다른 작업이 &quot;dev&quot;로 실행한 프로세스에 의존하지 못하게 막는다.&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;만약 persistne 설정을 안했다고 가정해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1755247993655&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// persistent 설정 없이
{
  &quot;tasks&quot;: {
    &quot;dev&quot;: {},
    &quot;test&quot;: {
      &quot;dependsOn&quot;: [&quot;dev&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;pre id=&quot;code_1755248023189&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;turbo run test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 실행했다고 가정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 test는 &quot;dev&quot; 에 의존하므로, turbo run dev 가 먼저 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 dev 작업은 끝나지 않는 작업이므로(개발 서버를 띄운 것이니까!)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test는 무한 대기를 하게 된다.&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;그러므로, persistent: true 설정을 통해 무한 대기를 방지하고, 해당 작업이 장기적으로 실행되는 작업임을 터보레포에게 알려주는 셈이다.&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;pre id=&quot;code_1755248430836&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;pipeline&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [
        &quot;^build&quot;,
        &quot;dev&quot; // dev에 의존
      ],
      &quot;outputs&quot;: [
        &quot;dist/**&quot;
      ]
    },
    &quot;dev&quot;: {
      &quot;cache&quot;: false
      // persistent 설정 없음
    },
    &quot;type-check&quot;: {
      &quot;dependsOn&quot;: [
        &quot;^build&quot;
      ]
    },
    &quot;lint&quot;: {
      &quot;dependsOn&quot;: [
        &quot;^build&quot;
      ]
    },
    &quot;test&quot;: {
      &quot;dependsOn&quot;: [
        &quot;^build&quot;
      ]
    },
    &quot;clean&quot;: {
      &quot;cache&quot;: false
    }
  }
}&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;그 후 turbo run build 를 실행하니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2053&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv6XLC/btsPWmYOyWM/ri7haTgCnK3PAJjxxuTID1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv6XLC/btsPWmYOyWM/ri7haTgCnK3PAJjxxuTID1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv6XLC/btsPWmYOyWM/ri7haTgCnK3PAJjxxuTID1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv6XLC%2FbtsPWmYOyWM%2Fri7haTgCnK3PAJjxxuTID1%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;2053&quot; height=&quot;461&quot; data-origin-width=&quot;2053&quot; data-origin-height=&quot;461&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;이렇게 dev가 완료되기를 무한 대기하고 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755248707186&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;type-check&quot;: {
  &quot;dependsOn&quot;: [
    &quot;^build&quot;
  ]
},
&quot;lint&quot;: {
  &quot;dependsOn&quot;: [
    &quot;^build&quot;
  ]
},
&quot;test&quot;: {
  &quot;dependsOn&quot;: [
    &quot;^build&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;위 내용은 각 명령어를 실행할 때, 의존성 build를 전부 끝내고 해당 명령어들이 실행된단 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 나의 경우, 백 , 프론트가 공통으로 사용하는 타입을 shared에 설정했기에, shared가 먼저 빌드 되어야 type-check, lint, test가 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755248831803&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;clean&quot;: {
  &quot;cache&quot;: false
}&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;clean의 경우, 빌드 결과물이나 캐시 등을 삭제해 프로젝트를 깨끗한 상태로 만들기 위한 명령어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 작업은 캐싱할 필요가 없으니, &quot;cache&quot;:false 로 설정했다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Typescript 기반의 풀스택 모노레포를 구성해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Turborepo를 사용하면 매우 쉽게 모노레포를 구성할 수 있으니, 모노레포 구성을 원하는 사람들이면 도전해보길 추천한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://turborepo.com/docs/reference/configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://turborepo.com/docs/reference/configuration&lt;/a&gt;&lt;/p&gt;</description>
      <category> ️ 내가 다시 볼 것</category>
      <author>전호영</author>
      <guid isPermaLink="true">https://securityinit.tistory.com/262</guid>
      <comments>https://securityinit.tistory.com/262#entry262comment</comments>
      <pubDate>Fri, 15 Aug 2025 18:20:45 +0900</pubDate>
    </item>
    <item>
      <title>www.naver.com 을 입력하면 무슨 일이 발생할까?</title>
      <link>https://securityinit.tistory.com/261</link>
      <description>&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;&quot;www.naver.com 을 입력하면 무슨 일이 발생하나요?&quot;&amp;nbsp;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다보니 내가 이해하기 쉽게 정리해보려 한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 이 글에선 렌더링 과정에 대한 내용은 다루지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 순수한 &lt;b&gt;네트워크 관점&lt;/b&gt;으로 정리 할 예정이다.&lt;/p&gt;
&lt;p 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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DNS 서버에 대해서 알아보자.&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소창에 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 을 입력했을 때 어떤 일이 일어나는지 알기 위해선, DNS 서버에 대해서 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 서버는 Domain Name Server 의 줄임말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소창에 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 을 치고 엔터를 누르면 네이버 웹 서버까지 TCP/IP 연결을 해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러려면 당연히! IP 주소를 알아야 한다.&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;그런데 우린 IP 주소를 모른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(실제로 IP 주소로 접속이 되지도 않는다.)&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;그래서 이 IP 주소를 누가 알고 있느냐?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 IP 주소를 알고 있는 데이터베이스가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 우리가 친구한테 전화할 때 진짜 전화번호는 잘 누르지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연락처에 있는 이름을 누르면 바로 전화가 간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 그대로 네트워크 개념으로 바꿔보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친구 이름이 도메인 네임이 되고, 전화번호부가 IP 주소를 알고 있는 데이터베이스가 된다.&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;이름으로 IPv4 주소를 알려주는 데이터베이스가 DNS(Domain Name Service) &lt;/b&gt;이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(요즘 세상은 모든 것이 웹 기반으로 동작한다. 그러니까 이 DNS는 매우 매우 매우 중요하다! 얘가 잘못되면 인터넷 전체가 잘못될 수 있다.)&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;그래서 DNS 서버가 무엇을 하냐 ?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 네임을 주면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;이거 IP 주소 뭐야 ?&quot; &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;&quot;어 그거 IP 주소는 ~~~ 야&quot;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고, 말해주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 데이터베이스(DNS) 는 &lt;b&gt;분산 구조형&lt;/b&gt;으로 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(대부분의 경우 트리구조이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 이해하려면 도메인 네임의 구조에 대해 이해할 필요가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;도메인 네임에 대해 알아보자.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt;&amp;nbsp;이란 도메인 네임이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 www 는 naver 에 속해있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;www.naver 는 com 에 속해있다.&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;인터넷 주소도 이와 똑같다!&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;그래서 www 는 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 &lt;b&gt;Host Name&lt;/b&gt; 이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 은 naver.com&amp;nbsp; 도메인에 속한 이름이 www 인 호스트를 의미한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우린 이 주소를 통째로 URL 주소라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 주로 우린 이 URL 주소를 통째로 DNS 서버에 물어보고, DNS 서버는 그 주소에 해당하는 IPv4 주소를 알려준다.&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;b&gt;그래서 DNS 가 왜 분산구조형인지 알아보자.&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btrwYs/btsNWh8leWC/ccPqEVvBZft6JXgUJh9lc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btrwYs/btsNWh8leWC/ccPqEVvBZft6JXgUJh9lc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btrwYs/btsNWh8leWC/ccPqEVvBZft6JXgUJh9lc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtrwYs%2FbtsNWh8leWC%2FccPqEVvBZft6JXgUJh9lc1%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;592&quot; height=&quot;306&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;a href=&quot;https://www.naver.com&quot;&gt;https://www.naver.com&lt;/a&gt; 이라 검색하면 어떤 일이 발생하는가?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단은 사용 중인 컴퓨터의 IP 설정을 따른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정엔 DNS 서버 주소가 포함되어 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS가 이 DNS 서버 주소에 질문한다.&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;&quot;야 나 네이버에 접속하려는데, &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt;&amp;nbsp;주소 좀 알려줘&quot;&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;그럼 DNS 서버에서 응답을 준다.&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;&quot;어 주소 ~~~ 야!&quot;&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;이렇게 받은 주소를 통해 네이버에 접속한다.(주로 HTTPS)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(물론 실제 네이버에 접속할 땐 로드밸런싱 등 여러 복잡한 과정을 거치지만 여기선 간단하게 이해하면 된다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l4xlU/btsN2RUcrKa/i6rRyXhfpK3bjiTvhpl7pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l4xlU/btsN2RUcrKa/i6rRyXhfpK3bjiTvhpl7pK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l4xlU/btsN2RUcrKa/i6rRyXhfpK3bjiTvhpl7pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl4xlU%2FbtsN2RUcrKa%2Fi6rRyXhfpK3bjiTvhpl7pK%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;591&quot; height=&quot;306&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 통신이 되면, 우리가 보는 네이버 화면이 뜨는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(참고로 DNS 서버 주소는 주로 ISP 에서 정해준 주소를 사용한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;만약 DNS 서버 응답 속도가 느려지면, 인터넷 전체가 느려지는 일이 발생한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;사용자 입장에선 네이버가 느린 것처럼 보이게 된다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&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;b&gt;DNS Cache&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 DNS에게 한 번이라도 질의를 하면, &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 의 IP 주소가 3.3.3.3 이라고 가정했을 때, 이 IP 주소를 PC가 메모리에다 저장을 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 PC 마다 DNS 요청 정보(도메인 네임 - IP 주소)를 저장하는 것을 &lt;b&gt;DNS Cache&lt;/b&gt; 라고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 DNS 서버에 응답을 한 뒤 받은 IP 는 항상 유효기간을 함께 준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, PC의 DNS Cache 는 DNS 서버가 준 유효기간만큼 DNS 정보를 저장하고, 유효기간이 지나면 만료시킨다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제부터 클라이언트가 특정 도메인 네임에 대한 IP 주소를 질의하면, DNS Cache를 먼저 살펴보게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 후 DNS Cache에 없다면, DNS 서버에 질의를 하게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;hosts 파일&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTHWAR/btsN26jgQh0/NAv87cAxU9rXkr30eQ31ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTHWAR/btsN26jgQh0/NAv87cAxU9rXkr30eQ31ok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTHWAR/btsN26jgQh0/NAv87cAxU9rXkr30eQ31ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTHWAR%2FbtsN26jgQh0%2FNAv87cAxU9rXkr30eQ31ok%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;566&quot; height=&quot;306&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;PC에는 hosts 파일이 존재한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 hosts 파일은 IP 주소와 URL을 적어놓은 파일이다. 만약 여기 URL과 이에 해당하는 IP 주소가 있다면 PC 는 DNS 서버에 물어보지 않고, hosts 파일에 있는 정보를 활용한다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;만약 그 누구도 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&lt;/a&gt; 에 질의한 적 없다면 어떤 일이 발생할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴 일은 없겠지만, 특정 ISP 사용자 중 단 한명도 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 의 주소를 물어본 적이 없다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 ISP 의 DNS 서버 설정에 따라 다르겠지만 보편적인 방법으로 이해해보자.&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;1. 유저가 DNS 서버에 주소를 요청한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. DNS 서버의 캐시 확인&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;그림을 보면 DNS 서버에 Cache라고 적혀있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ISP의 DNS 서버도 캐싱을 하는가?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;ISP의 DNS 서버(이하 Cache DNS)는 먼저 자신의 캐시에 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 에 대한 IP 정보가 있는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 캐시에 정보가 없다면, 계층적으로 DNS 질의 과정을 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 얘는 어떻게 캐시를 하냐면..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 계층적 질의 과정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지구상의 DNS 서버는 계층적인 구조를 갖고 있는데, 그 정점에 RootDNS 가 있다. (약 13대가 존재하다.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Root DNS 질의
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cache DNS 서버가 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 에 해당하는 IP 주소를 가지고 있지 않다면,&amp;nbsp; Root DNS에 요청을 한다.&lt;br /&gt;&quot;RootDNS 야 , 누가&amp;nbsp;&lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 주소를 물어봤는데, com 에 해당하는 DNS 좀 알려줘&quot; 라고 요청하면,&lt;br /&gt;Root DNS는 com 도메인을 관리하는 TLD(Top Level Domain) DNS 서버 주소 목록을 알려준다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TLD DNS 질의
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cache DNS는 com TLD DNS 서버 중 하나에 다시 질의한다.&lt;br /&gt;&quot;Naver에 해당하는 주소를 알려줘!&quot; 라고 요청하면,&lt;br /&gt;com TLD DNS 서버는 naver.com 도메인을 관리하는 네임서버의 정보를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Authroitative DNS 질의
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cache DNS 는 naver.com 의 네임서버에 www.naver.com의 IP 주소를 요청한다.&lt;br /&gt;그럼 네임서버는 해당하는 IP 주소를 Cache DNS에 돌려준다.&lt;/li&gt;
&lt;li&gt;Authoritative DNS란 특정 도메인(예를 들어 naver.com) 에 대한 공식적인 DNS 정보를 직접 가지고 있으며, 해당 도메인에 대한 질의에 대해 최종으로 응답을 제공하는 네임서버이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 캐싱 및 응답&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache DNS는 이렇게 얻은 IP 주소 정보를 함께 받은 만료기간과 함께 캐시에 저장한다.&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;b&gt;&amp;nbsp;그래서 면접에서 어떻게 대답하면 될까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 면접관이 &quot;www.naver.com&quot;에 접속하면 어떤 일이 발생하는지 설명해주세요! 라고 물어본다면 다음과 같이 답해보자.&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;&quot;먼저, PC는 운영체제의 hosts 파일에 url 주소와 이에 해당하는 ip 주소가 매핑되어 있는지 확인합니다. 있다면 해당 정보를 사용하고, 없다면 로컬 DNS 캐시를 확인합니다. OS의 DNS 캐시(로컬 DNS 캐시)에 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&lt;/a&gt; 의 IP 주소가 저장되어 있는지 확인하는 작업입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 DNS 캐시에 정보가 없다면, PC는 네트워크 설정에 있는 ISP DNS 서버 주소에 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&lt;/a&gt; 의 IP 주소 정보를 질의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISP DNS 서버가 &lt;a href=&quot;http://www.naver.com에&quot;&gt;www.naver.com에&lt;/a&gt; 해당하는 IP 주소를 가지고 있다면, 이 주소를 요청 클라이언트에게 반환하고, 없다면 계층적 질의 작업을 통해 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&lt;/a&gt; 의 IP 주소를 알아온 뒤, 요청한 클라이언트에게 알려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계층 질의 과정은 다음 순서로 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Root DNS에 com 도메인을 관리하는 TLD DNS 정보를 요청합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;com TLD DNS 서버 중 하나에 naver.com 의 네임 서버를 요청합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;이렇게 받은 &lt;a href=&quot;http://naver.com&quot;&gt;naver.com&lt;/a&gt; 네임서버에 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 의 IP 주소를 요청합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;이 과정을 거쳐 받은 ip 주소를 만료기간과 함께 ISP DNS 서버에 저장하고, PC에 주소를 전달합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;이 후, PC는 해당 IP 주소를 활용해 &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 웹 서버와 TCP 연결을 맺고, 이후 https 프로토콜로 통신을 합니다.&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;핵심만 외워보자&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PC는 ip 주소를 획득할 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hosts 파일 -&amp;gt; 로컬 DNS 캐시 -&amp;gt; DNS 서버 순으로 확인한다!!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;참고자료&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/windows-server/networking/dns/dns-architecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://learn.microsoft.com/en-us/windows-server/networking/dns/dns-architecture&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://blog.devops.dev/understanding-name-resolution-on-a-network-from-hosts-file-to-dns-2d8efb7cdb38&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;http://blog.devops.dev/understanding-name-resolution-on-a-network-from-hosts-file-to-dns-2d8efb7cdb38&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nordvpn.com/ko/blog/dns-cache/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nordvpn.com/ko/blog/dns-cache/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/route53/what-is-dns/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aws.amazon.com/ko/route53/what-is-dns/&lt;/a&gt;&lt;/p&gt;</description>
      <category> ️ 내가 다시 볼 것</category>
      <author>전호영</author>
      <guid isPermaLink="true">https://securityinit.tistory.com/261</guid>
      <comments>https://securityinit.tistory.com/261#entry261comment</comments>
      <pubDate>Sat, 17 May 2025 23:01:37 +0900</pubDate>
    </item>
    <item>
      <title>대답하지 못한 기술 면접 정리하기(CIDR, 방화벽, DMZ)</title>
      <link>https://securityinit.tistory.com/260</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 기술면접을 보며 대답을 잘 하지 못했던 내용에 대해 정리해보려 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 192.168.1.100/26 대역에서 전체 할당 가능한 IP 는 몇 개이고, 실제 호스트에 할당 가능한 IP는 몇 개인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;192.168.1.100/26 처럼 IP 주소 뒤에 / 와 숫자를 붙여 네트워크 비트의 개수를 나타내는 방식을 CIDR 이라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시엔 26이 붙어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 앞의 26비트가 네트워크 주소임을 의미한다.&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;IPv4 주소는 32비트이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 앞의 26비트가 네트워크 주소이므로, &lt;b&gt;호스트가 사용할 수 있는 비트는 6 비트이다.&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;2^6 개가 전체 할당 가능한 IP 개수이다. (64개)&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;그런데 실제 호스트에 할당 가능한 IP는 전체에서 네트워크 주소와 브로드캐스트 주소를 빼야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 호스트는 2개를 뺀 &lt;b&gt;62개를 사용할 수 있다.&amp;nbsp;&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;b&gt;여기서 네트워크 주소와 브로드캐스트 주소란 무엇일까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;네트워크 주소&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 IP 대역(서브넷)에서 호스트 부분이 모드 0인 주소로, 해당 서브넷의 &quot;시작 주소&quot;가 된다.&lt;/li&gt;
&lt;li&gt;192.168.0/24 에선 192.168.1.0 이 네트워크 주소가 된다.&lt;/li&gt;
&lt;li&gt;실제로 할당하진 않고, 네트워크 구간을 대표하는 용도로만 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;브로드캐스트 주소&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 IP 대역(서브넷)에서 호스트 부분이 모두 1인 주소로, 같은 서브넷에 속한 모든 호스트에게 동시에 데이터를 전송할 때 사용한다.&lt;/li&gt;
&lt;li&gt;192.168.0/24 에선 192.168.1.255 가 브로드캐스트 주소이다.&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. DMZ의 역할은 무엇이고, DMZ엔 어떤 서버를 넣어야 하는가?&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSEiqC/btsNJTS5cKq/viiRjpJnm97q6y1fyJknck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSEiqC/btsNJTS5cKq/viiRjpJnm97q6y1fyJknck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSEiqC/btsNJTS5cKq/viiRjpJnm97q6y1fyJknck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSEiqC%2FbtsNJTS5cKq%2FviiRjpJnm97q6y1fyJknck%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;464&quot; height=&quot;554&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네크워크 상황은 위와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DMZ(Demiliatrized Zone)&lt;/b&gt;의 약어로 , 기업이나 조직의 &lt;b&gt;내부 네트워크와 외부 네트워크 사이에 위치한 중간 지대&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 네트워크와 외부 네트워크 사이의 통신을 관리하고, 보안을 강화하기 위해 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. DMZ는 어떤 역할을 할까?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서 접근할 수 있는 서비스를 내부망과 분리하여 외부 공격자가 DMZ 내부에 침투하더라도 내부 네트워크로의 직접적인 침입을 어렵게 한다.&lt;/li&gt;
&lt;li&gt;외부 사용자가 접근해야 하는 서비스를 DMZ에 배치하여, 내부망을 보호하는 동시에, 서비스를 제공한다.&lt;/li&gt;
&lt;li&gt;DMZ, 내부망, 외부망을 각 방화벽을 통해 다른 보안 정책을 적용할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;DMZ에 방화벽, IDS/IPS 등 다양한 보안 장비를 배치하여, 외부에서 공격을 탐지하고, 차단한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;그래서 DMZ엔 어떤 서버를 두어야 할까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DMZ의 역할엔 외부 서비스와 내부 서비스를 분리하여 내부 서비스를 보호하는 것이 있다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 서버&lt;/li&gt;
&lt;li&gt;메일 서버&lt;/li&gt;
&lt;li&gt;DNS 서버&lt;/li&gt;
&lt;li&gt;FTP 서버&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;API Gateway , 로드밸런서, 인증 서버 등 외부 요청을 내부 서버로 중계하는 역할을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 WAS, DB 서버는 내부망에 두어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직과 민감한 데이터는 외부로 유출시키면 안되므로!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 D&lt;b&gt;MZ 공격자가 네트워크로 침입하더라도, 내부망이 안전하도록 두는 완충지대이다.&lt;/b&gt;&lt;/p&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;b&gt;2. 그럼 외부 방화벽과 내부 방화벽엔 각각 어떤 역할을 주어야 할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 두 방화벽이 어떤 역할을 하는지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;외부 방화벽&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터넷(외부망)과 DMZ/내부망 사이에 위치한다.&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;외부에서 들어오는 불필요한 트래픽을 1차 차단&lt;/li&gt;
&lt;li&gt;DMZ 또는 내부망으로의 직접적인 접근을 제한한다.&lt;/li&gt;
&lt;li&gt;DMZ에 있는 서비스로만 접근 가능하도록 포트, IP 기반 접근을 제어한다.&lt;/li&gt;
&lt;li&gt;DDoS, 포트 스캐닝 등 외부의 위협에 대해 1차 방어를 한다.&lt;/li&gt;
&lt;li&gt;로그를 수집하고, 이상 트래픽이 있다면 탐지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; 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;&lt;b&gt;내부 방화벽&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DMZ와 내부망 사이에 위치한다.&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;DMZ가 침해당했을 때, 내부망으로 추가 침입을 방지한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;DMZ에서 내부망으로의 트래픽을 제한한다.(특정 포트만 열어준다던가, 특정 서버만 열어준다던가..)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;내부에서 외부로 나가는 트래픽은 필요에 따라 허용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category> ️ 내가 다시 볼 것</category>
      <category>네트워크</category>
      <author>전호영</author>
      <guid isPermaLink="true">https://securityinit.tistory.com/260</guid>
      <comments>https://securityinit.tistory.com/260#entry260comment</comments>
      <pubDate>Fri, 2 May 2025 22:29:36 +0900</pubDate>
    </item>
    <item>
      <title>서울우유 업무 프로세스 개선 프로젝트 공지 조회 서비스 성능 개선기</title>
      <link>https://securityinit.tistory.com/259</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서울우유와 함께 한 기업프로젝트가 끝났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현이 끝났으니, 성능을 측정해보고 개선을 해보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선을 해볼 API는 공지사항 조회이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 모든 유저가 접근할 수 있는 API이기도 하고, 읽기 작업이 주를 이루는 API이므로 선택했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2119&quot; data-origin-height=&quot;1315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHCn3d/btsMZrC2mdd/FftsWwkPxKPCArV4sE5K9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHCn3d/btsMZrC2mdd/FftsWwkPxKPCArV4sE5K9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHCn3d/btsMZrC2mdd/FftsWwkPxKPCArV4sE5K9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHCn3d%2FbtsMZrC2mdd%2FFftsWwkPxKPCArV4sE5K9k%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;2119&quot; height=&quot;1315&quot; data-origin-width=&quot;2119&quot; data-origin-height=&quot;1315&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;성능 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능을 개선하기 전에 어떤 지점에서 조회 성능이 떨어지는지를 확인해보자!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1423&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uzJp9/btsM0eXnZLd/ghJ2Y5zTBF0q5xUCol01xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uzJp9/btsM0eXnZLd/ghJ2Y5zTBF0q5xUCol01xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uzJp9/btsM0eXnZLd/ghJ2Y5zTBF0q5xUCol01xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuzJp9%2FbtsM0eXnZLd%2FghJ2Y5zTBF0q5xUCol01xK%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;1423&quot; height=&quot;514&quot; data-origin-width=&quot;1423&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서울우유 측에서 받은 정보이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 업무를 처리하는 인원이 50명이므로, 테스트 시 인원 수 Max를 50으로 맞췄다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1 페이지에만 접근하기&lt;/b&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;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;page == 1 인 API에 대해 성능을 체크해보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;request &amp;nbsp;성공 여부 고려&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;11000 건 데이터&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TJZqD/btsMY9XmpoZ/4h3PPs7k8klQumQ1GMyx8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TJZqD/btsMY9XmpoZ/4h3PPs7k8klQumQ1GMyx8K/img.png&quot; data-alt=&quot;11000 건 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TJZqD/btsMY9XmpoZ/4h3PPs7k8klQumQ1GMyx8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTJZqD%2FbtsMY9XmpoZ%2F4h3PPs7k8klQumQ1GMyx8K%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;1480&quot; height=&quot;620&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;11000 건 데이터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;1000000건 데이터(&lt;b&gt;&lt;b&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-origin-width=&quot;1469&quot; data-origin-height=&quot;617&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb8H2J/btsM1qb9lAr/ZKkGiUiJ2r9gD876fZlndK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb8H2J/btsM1qb9lAr/ZKkGiUiJ2r9gD876fZlndK/img.png&quot; data-alt=&quot;1000000 건 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb8H2J/btsM1qb9lAr/ZKkGiUiJ2r9gD876fZlndK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb8H2J%2FbtsM1qb9lAr%2FZKkGiUiJ2r9gD876fZlndK%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;1469&quot; height=&quot;617&quot; data-origin-width=&quot;1469&quot; data-origin-height=&quot;617&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;1000000 건 데이터&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;10000000건 데이터 &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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1439&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yUFha/btsMY7Mtxt6/kdFXbsYsNnmk2E2lQPywv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yUFha/btsMY7Mtxt6/kdFXbsYsNnmk2E2lQPywv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yUFha/btsMY7Mtxt6/kdFXbsYsNnmk2E2lQPywv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyUFha%2FbtsMY7Mtxt6%2FkdFXbsYsNnmk2E2lQPywv1%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;1439&quot; height=&quot;559&quot; data-origin-width=&quot;1439&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; 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;&lt;b&gt;request &amp;nbsp;성공 여부 + 응답 시간을 함께 고려&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1367&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q4ers/btsM0b7tOxw/KVomR5965KFaOl3MJYhax0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q4ers/btsM0b7tOxw/KVomR5965KFaOl3MJYhax0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q4ers/btsM0b7tOxw/KVomR5965KFaOl3MJYhax0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ4ers%2FbtsM0b7tOxw%2FKVomR5965KFaOl3MJYhax0%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;1367&quot; height=&quot;256&quot; data-origin-width=&quot;1367&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글에선 응답 시간을 0.5초 미만으로 줄이는 것이 목표이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 따라 우리 API 의 응답 기준도 0.5초로 맞추고 테스트를 진행해보았다.&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;&lt;b&gt;1000000건 데이터(백만건)&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckDAqX/btsMZT01F7D/hWQUK6A13DxBnV413czBYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckDAqX/btsMZT01F7D/hWQUK6A13DxBnV413czBYk/img.png&quot; data-alt=&quot;백만건&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckDAqX/btsMZT01F7D/hWQUK6A13DxBnV413czBYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckDAqX%2FbtsMZT01F7D%2FhWQUK6A13DxBnV413czBYk%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;1492&quot; height=&quot;700&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;백만건&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;백만건의 데이터를 넘어가자 응답시간이 0.5초를 초과하는 데이터가 나오기 시작했다.&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;10000000건 데이터 &lt;b&gt;&lt;b&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-origin-width=&quot;1459&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNclmG/btsM1qDg6AO/KGl8ZgL0A6TD9jjWDh8GV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNclmG/btsM1qDg6AO/KGl8ZgL0A6TD9jjWDh8GV0/img.png&quot; data-alt=&quot;천만건&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNclmG/btsM1qDg6AO/KGl8ZgL0A6TD9jjWDh8GV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNclmG%2FbtsM1qDg6AO%2FKGl8ZgL0A6TD9jjWDh8GV0%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;1459&quot; height=&quot;361&quot; data-origin-width=&quot;1459&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;천만건&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번의 요청을 제외하고, 전부 0.5초 이상 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http_req_duration 을 보면 첫 페이지에 접근하는데 평균 9.62초가 걸렸다.&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;이제 개선을 해보자!&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;불필요한 정렬 제거&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;1294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg4HxB/btsM1PJCTen/k5H2yYMGbXRpWsA3Mr2xDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg4HxB/btsM1PJCTen/k5H2yYMGbXRpWsA3Mr2xDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg4HxB/btsM1PJCTen/k5H2yYMGbXRpWsA3Mr2xDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg4HxB%2FbtsM1PJCTen%2Fk5H2yYMGbXRpWsA3Mr2xDk%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;135&quot; height=&quot;385&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;1294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 요청을 보낼 때 JPA 쿼리이다.&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;현재 정렬을 중복으로 2번 하고 있는데, 이 부분을 1번으로 줄이면 조금의 개선을 얻을 수 있어 보인다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Pageable 을 인자로 넘겨주면 기본적으로 내림차순 정렬을 한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 난 그 사실을 모르고, @Query에서 한 번 더 정렬을 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1743354466507&quot; class=&quot;n1ql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Query(&quot;SELECT n FROM NoticeJpaEntity n ORDER BY n.id DESC&quot;)
Page&amp;lt;NoticeJpaEntity&amp;gt; findAllOrderByIdDesc(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드의&lt;/p&gt;
&lt;pre id=&quot;code_1743354466510&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Query(&quot;SELECT n FROM NoticeJpaEntity n&quot;)
Page&amp;lt;NoticeJpaEntity&amp;gt; findAllOrderByIdDesc(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정렬을 없애주니, 정렬이 한 번만 나갔다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4vC9a/btsM0NFUAp0/j3clK4WMebHP08DsYTI3LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4vC9a/btsM0NFUAp0/j3clK4WMebHP08DsYTI3LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4vC9a/btsM0NFUAp0/j3clK4WMebHP08DsYTI3LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4vC9a%2FbtsM0NFUAp0%2Fj3clK4WMebHP08DsYTI3LK%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;227&quot; height=&quot;397&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지네이션 부분의 실행계획을 분석해보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;페이지네이션과 실행계획 분석&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1743145270560&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN PLAN FOR
select
    nje1_0.id,
    nje1_0.author_name,
    nje1_0.author_pk,
    nje1_0.content,
    nje1_0.created_at,
    nje1_0.deleted,
    nje1_0.file_url,
    nje1_0.title,
    nje1_0.updated_at
from
    notice nje1_0
order by
    nje1_0.id desc
    fetch
    first :take rows only;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3bfGU/btsM1dkUXOd/KtGhGSeB92KuNktasUDCP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3bfGU/btsM1dkUXOd/KtGhGSeB92KuNktasUDCP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3bfGU/btsM1dkUXOd/KtGhGSeB92KuNktasUDCP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3bfGU%2FbtsM1dkUXOd%2FKtGhGSeB92KuNktasUDCP0%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;970&quot; height=&quot;490&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 NOTICE 에 대한 풀 스캔을 하고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아무 인덱스가 걸려있지 않기에, 페이지네이션을 커스텀해보고, 인덱스를 사용해 성능을 개선해보려 한다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&amp;nbsp;페이지네이션 커스텀&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 우리 조회 코드에선&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Pageable&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 사용한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Pageable 을 사용하면,&amp;nbsp;DB의 limit 과 Offset 쿼리를 통해 '페이지' 단위로 데이터를 구분한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;페이징을 구현하기 위해 '전체 데이터 개수' 를 가져와 전체 페이지 개수를 계산하고, 현재 페이지가 첫 번째인지, 마지막 페이지인지 계산해야한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 과정으로 인해&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쿼리가 2회 나간다. (데이터 요청, 데이터 개수 카운트)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는 페이지가 뒤로 갈 수록 읽고, 계산해야 할 양이 많아지기에 성능이 저하된다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;랜덤하게 조회 요청을 보냈을 때 Timeout이 온 것을 보면 알 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;1063&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTxejf/btsMZf4hEQr/PZsQkna4aNPasZlREdBgI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTxejf/btsMZf4hEQr/PZsQkna4aNPasZlREdBgI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTxejf/btsMZf4hEQr/PZsQkna4aNPasZlREdBgI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTxejf%2FbtsMZf4hEQr%2FPZsQkna4aNPasZlREdBgI0%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;238&quot; height=&quot;318&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;1063&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리를 보고도 알 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 페이지네이션을 커스텀 해보았다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743330502847&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN PLAN FOR
SELECT
    n.id,
    n.author_name,
    n.title,
    n.created_at
FROM notice n
ORDER BY n.id DESC
OFFSET :skip ROWS FETCH NEXT :take ROWS ONLY;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;페이지네이션 코드의 실행계획을 분석해보자.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPvclf/btsM1RIB6YB/Cgq6NZSojSvbQaapXpJ6p1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPvclf/btsM1RIB6YB/Cgq6NZSojSvbQaapXpJ6p1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPvclf/btsM1RIB6YB/Cgq6NZSojSvbQaapXpJ6p1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPvclf%2FbtsM1RIB6YB%2FCgq6NZSojSvbQaapXpJ6p1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;252&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 풀스캔을 하고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 인덱스를 통해 수정해보려 했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;페이지 기반 페이지네이션의 경우 OFFSET을 사용한다. 이는 테이블을 풀 스캔 한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 인덱스를 둬도, 테이블을 풀 스캔하기에 우리가 필요한 값만 커버링 인덱스를 만들어 사용해보자.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743331070688&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN PLAN FOR
SELECT /*+ INDEX_DESC(n idx_notice_covering) */
    n.id,
    n.author_pk,
    n.author_name,
    n.title,
    n.content,
    n.file_url,
    n.created_at,
    n.updated_at,
    n.deleted
FROM notice n
ORDER BY n.id DESC
OFFSET :skip ROWS FETCH NEXT :take ROWS ONLY;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 문제는 content가 CLOB 타입이란 점이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CLOB은 인덱스에 포함될 수 없다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 외에 여러 인덱스를 걸어보았지만, 성능이 더 나아지는 경우가 없었다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;성능 개선을 하기위해선 불필요한 ORDER를 없애거나, 인덱스를 적용해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 페이지 기반 페이지네이션을 사용하게 되면, OFFSET을 쓸 수 밖에 없다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;OFFSET의 특성 상 풀 스캔을 할 수 밖에 없고, 페이지 기반 페이지네이션의 경우 어디에 인덱스를 걸어야 하는지도 명확하지 않았다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 인덱스와 커스텀 페이지네이션을 사용하기보다, Pageable 객체를 사용하고 캐싱을 통해 성능을 개선하는 방법을 선택했다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;만약 커서 기반 페이지네이션을 사용한다면?&lt;/b&gt;&lt;/h3&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 우리 조회 코드에선&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Pageable&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 사용한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Pageable 을 사용하면,&amp;nbsp;DB의 limit 과 Offset 쿼리를 통해 '페이지' 단위로 데이터를 구분한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;페이징을 구현하기 위해 '전체 데이터 개수' 를 가져와 전체 페이지 개수를 계산하고, 현재 페이지가 첫 번째인지, 마지막 페이지인지 계산해야한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 과정으로 인해&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쿼리가 2회 나간다. (데이터 요청, 데이터 개수 카운트)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는 페이지가 뒤로 갈 수록 읽고, 계산해야 할 양이 많아지기에 성능이 저하된다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 커서 기반 페이지네이션으로 변경했다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;863&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qKNAp/btsM1gWn895/B8YnH3IMBSKD3qTqPviIlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qKNAp/btsM1gWn895/B8YnH3IMBSKD3qTqPviIlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qKNAp/btsM1gWn895/B8YnH3IMBSKD3qTqPviIlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqKNAp%2FbtsM1gWn895%2FB8YnH3IMBSKD3qTqPviIlk%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;220&quot; height=&quot;422&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;863&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리가 더 간결해진 것을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리의 실행계획을 분석해보자!&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;1021&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VRZjO/btsM1QCW1zS/1adzyDTlTzj06s54ijPgI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VRZjO/btsM1QCW1zS/1adzyDTlTzj06s54ijPgI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VRZjO/btsM1QCW1zS/1adzyDTlTzj06s54ijPgI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVRZjO%2FbtsM1QCW1zS%2F1adzyDTlTzj06s54ijPgI1%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;1560&quot; height=&quot;1021&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;1021&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 INDEX가 걸려있지 않아 , 풀 테이블 스캔을 하고 있다(3).&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 추가적인 정렬 작업이 발생한다(2).&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 인덱스를 적용해보자!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743355218349&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_notice_id ON notice (id DESC);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1933&quot; data-origin-height=&quot;705&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3JgYi/btsM3kW46S2/2Kq8Q6z1rbQfyF6QbzsfKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3JgYi/btsM3kW46S2/2Kq8Q6z1rbQfyF6QbzsfKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3JgYi/btsM3kW46S2/2Kq8Q6z1rbQfyF6QbzsfKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3JgYi%2FbtsM3kW46S2%2F2Kq8Q6z1rbQfyF6QbzsfKK%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;1933&quot; height=&quot;705&quot; data-origin-width=&quot;1933&quot; data-origin-height=&quot;705&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;성능이 조금 좋아지긴 했지만, 아직도 불필요한 정렬 작업을 수행하고 있다.(SORT ORDER BY)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;내가 id DESC 로 인덱스를 생성했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 DESC 내림차순 인덱스를 생성하면 내부적으로 함수 기반 인덱스로 처리된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로 인덱스는 함수 결과값으로 정렬되어 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러므로 SORT ORDER BY 가 먼저 실행이 된다. &amp;lt;- 여기서 불필요한 정렬이 발생한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 기본 id 인덱스(오름차순)으로 정렬하고, 힌트를 사용했다.&lt;/p&gt;
&lt;pre id=&quot;code_1743355218354&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_notice_id ON notice (id);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1743355218357&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN PLAN FOR
SELECT /*+ INDEX_RC_DESC(n idx_notice_id) */
    n.id,
    n.author_pk,
    n.author_name,
    n.title,
    n.content,
    n.file_url,
    n.created_at,
    n.updated_at,
    n.deleted
FROM notice n
WHERE n.id &amp;lt; :key
ORDER BY n.id DESC
    FETCH FIRST :take ROWS ONLY;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실행계획을 보니&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;657&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D11Xb/btsM2dRRZOL/VVvzROIWdI4Z6pbncplx3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D11Xb/btsM2dRRZOL/VVvzROIWdI4Z6pbncplx3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D11Xb/btsM2dRRZOL/VVvzROIWdI4Z6pbncplx3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD11Xb%2FbtsM2dRRZOL%2FVVvzROIWdI4Z6pbncplx3k%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;1602&quot; height=&quot;657&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;657&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전체 코스트가 매우매우 줄어들었다!!&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가로 필요한 데이터만 반환하도록 쿼리를 수정한 뒤, 실행계획을 분석해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1743355218363&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN PLAN FOR
SELECT /*+ INDEX_RC_DESC(n idx_notice_id) */
    n.id,
    n.title,
    n.author_name,
    n.created_at
FROM notice n
WHERE n.id &amp;lt; :key
ORDER BY n.id DESC
    FETCH FIRST :take ROWS ONLY;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;661&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4jg68/btsM1g9Yz9h/qH5f755Us0ONIyT8QfSgcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4jg68/btsM1g9Yz9h/qH5f755Us0ONIyT8QfSgcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4jg68/btsM1g9Yz9h/qH5f755Us0ONIyT8QfSgcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4jg68%2FbtsM1g9Yz9h%2FqH5f755Us0ONIyT8QfSgcK%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;1604&quot; height=&quot;661&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;661&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;사용하는 공간(Bytes)도 줄어들었다!&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;레포지토리 쿼리도 인덱스를 사용하도록 변경했다.&lt;/p&gt;
&lt;pre id=&quot;code_1743355218368&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(value = &quot;&quot;&quot;
SELECT /*+ INDEX_RS_DESC(n idx_notice_id) */
    n.id,
    n.title,
    n.author_name,
    n.created_at,
FROM notice n
WHERE n.id &amp;lt; ?1
ORDER BY n.id DESC
FETCH FIRST ?2 ROWS ONLY
&quot;&quot;&quot;, nativeQuery = true)
List&amp;lt;NoticeJpaEntity&amp;gt; findAllByPaginationDesc(Long key, Long take);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다시 k6로 테스트를 돌려보았다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1475&quot; data-origin-height=&quot;651&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eFSkij/btsM0wZttbx/sb6cIekDAR1tK6Z4kQk2M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eFSkij/btsM0wZttbx/sb6cIekDAR1tK6Z4kQk2M0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eFSkij/btsM0wZttbx/sb6cIekDAR1tK6Z4kQk2M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeFSkij%2FbtsM0wZttbx%2Fsb6cIekDAR1tK6Z4kQk2M0%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;1475&quot; height=&quot;651&quot; data-origin-width=&quot;1475&quot; data-origin-height=&quot;651&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;천만건의 데이터에서 1페이지의 경우, 모든 요청이 500ms 안으로 들어왔다!&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;천만건의 데이터에서 랜덤하게 요청을 보내보자.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;커서 기반 페이지네이션엔 페이지가 없기에 , 임의로 몇 개의 랜덤 url을 선정해 요청을 보냈다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1463&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FiEyj/btsM0rKCUhp/BFBr9vnD1kfGrKv9dtWk4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FiEyj/btsM0rKCUhp/BFBr9vnD1kfGrKv9dtWk4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FiEyj/btsM0rKCUhp/BFBr9vnD1kfGrKv9dtWk4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFiEyj%2FbtsM0rKCUhp%2FBFBr9vnD1kfGrKv9dtWk4k%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;1463&quot; height=&quot;640&quot; data-origin-width=&quot;1463&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모든 요청이 500ms 안으로 들어왔다!&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;캐시&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 빠른 조회를 위해 캐싱을 사용해보자!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 사용했지만 유의미한 결과를 얻지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 사용하면 더욱 빨라질 것이라 생각해 캐시도 적용해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 캐시를 사용함에 있어 2가지 선택지를 고민했다.&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;일반적으로 대규모 회사나 MSA 환경에선 여러 서비스가 분리되어 돌아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 로컬 캐시를 사용하게 되면, 캐시 간의 정합성이 깨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버 간 데이터 공유가 안되기에 캐싱된 데이터에 따라 서버 간 데이터 불일치가 발생한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다보니 Redis와 같은 글로벌 캐시를 사용하게 된다.&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;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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;조회를 위해 네트워크를 트래픽으로 사용하지 않는다.&lt;/li&gt;
&lt;li&gt;외부 서비스를 사용하지 않기에 지연 의존이 떨어진다.&lt;/li&gt;
&lt;li&gt;서버 리소스 효율성이 올라가 서버 성능이 개선된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 현재 우리의 시스템은 모놀리틱의 형태이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서울우유의 경우 MSA 환경을 사용할 가능성이 높다고 생각했다.&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;b&gt;2계층 캐시&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2계층 캐시는 동일한 요청에 대해 로컬 캐시를 조회하고 있으면 반환, 없다면 분산 캐시를 조회하여 그 결과를 반환하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2계층 캐시를 사용했을 때 장점은 글로벌 캐시(분산 캐시)로의 과도한 트래픽을 보호할 수 있다. 그리고 분산 캐시가 다운되더라도, 로컬 캐시를 사용할 수 있기에 서비스에 큰 문제를 막을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;구현&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 캐시로 Redis, 로컬 캐시로 Caffeine 을 사용하기로 결정했다.&lt;/p&gt;
&lt;pre id=&quot;code_1743349806843&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
RedisCacheConfiguration redisCacheConfiguration(ObjectMapper objectMapper) {
    return RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(
                    RedisSerializationContext.SerializationPair.fromSerializer(
                            new Jackson2JsonRedisSerializer&amp;lt;&amp;gt;(objectMapper, PageNoticeResponse.class)
                    )).entryTtl(Duration.ofMinutes(10));
}&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;먼저 Redis 설정을 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬화 설정을 추가해주고, 만료시간은 10분으로 줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 캐시에 대한 설정도 추가해줬다.&lt;/p&gt;
&lt;pre id=&quot;code_1743349986549&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public CacheManager customCacheManager(RedisConnectionFactory redisConnectionFactory, RedisCacheConfiguration redisCacheConfiguration) {

    CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
    caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(Duration.ofMinutes(5)));

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CacheManager 를 커스텀해 2계층 캐시를 구현한 뒤 , 이를 Configuration에 추가했다.&lt;/p&gt;
&lt;pre id=&quot;code_1743350273691&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public CacheManager customCacheManager(RedisConnectionFactory redisConnectionFactory, RedisCacheConfiguration redisCacheConfiguration) {

    CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
    caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(Duration.ofMinutes(5)));

    RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(redisCacheConfiguration)
            .build();

    return new CustomCacheManager(caffeineCacheManager, redisCacheManager);
}&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;캐시에 저장할 때 2계층 캐시를 제대로 활용하기 위해 Cache 구현체를 만들어 주었다!&lt;/p&gt;
&lt;pre id=&quot;code_1743350461538&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record CustomCache(Cache firstLevelCache, Cache secondLevelCache) implements Cache {
	...
    
    @Override
    public &amp;lt;T&amp;gt; T get(Object key, Class&amp;lt;T&amp;gt; type) {
        T value = firstLevelCache.get(key, type);
        if (value == null) {
            value = secondLevelCache.get(key, type);
            if (value != null) {
                firstLevelCache.put(key, value);
            }
        }
        return value;
    }

    @Override
    public void put(Object key, Object value) {
        firstLevelCache.put(key, value);
        secondLevelCache.put(key, value);
    }
    
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 성능 개선의 목표인 페이지네이션 코드에 캐시를 적용해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743350937313&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Cacheable(
        value = &quot;notice&quot;,
        cacheManager = &quot;customCacheManager&quot;,
        keyGenerator = &quot;noticePageableKeyGenerator&quot;
)
public PageNoticeResponse&amp;lt;NoticeSummaryResponse&amp;gt; getNoticesByPage(Pageable pageable) {
    cacheService.checkCacheContent();
    log.info(&quot;[ReadNoticeService.getNoticesByPage] 공지사항 페이지를 조회합니다.&quot;);
    Page&amp;lt;Notice&amp;gt; notices = noticeRepository.findAllOrderByIdDesc(pageable);
    List&amp;lt;NoticeSummaryResponse&amp;gt; content = notices.getContent().stream()
            .map(NoticeSummaryResponse::create).toList();

    return PageNoticeResponse.create(content, notices);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;read 요청이 들어 올 경우, 캐시를 읽도록 설정한다.&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;만약 공지사항에 새로운 공지를 추가하고, 캐시를 evict 하는 로직이다.&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;이런 경우를 막기 위해 커밋 시 event를 발행하고 해당 event가 발행되면 캐시가 evict 되도록 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1743351162851&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public PostNoticeResponse post(CustomUserDetails customUserDetails, PostNoticeRequest postNoticeRequest, MultipartFile file) {
    String fileUrl = fileUtil.uploadFile(file);
    try {
        Notice notice = noticeRepository.save(
                Notice.create(customUserDetails.getId(), customUserDetails.getUsername(), postNoticeRequest.title(), postNoticeRequest.content(), fileUrl)
        );
        postNoticeEventPublisher.publishPostNoticeEvent();
        return PostNoticeResponse.create(notice);
    } catch (Exception e) {
        log.error(&quot;[PostNoticeService.post] 공지사항을 등록하는데 실패했습니다. {}&quot;, e.getMessage());
        throw NoticeErrorCode.POST_NOTICE_FAILED.toException();
    }
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@CacheEvict(
        value = &quot;notice&quot;,
        cacheManager = &quot;customCacheManager&quot;,
        allEntries = true
)
public void evictNoticePageableCache(PostNoticeEvent postNoticeEvent) {
    log.info(&quot;[PostNoticeService.evictNoticePageableCache] 공지사항 페이지 캐시를 삭제합니다.&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 구현했으니, 실제 성능을 테스트해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;285&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nPORT/btsM1j6fyu3/xMomRDUcyGuEyDDKfPzC9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nPORT/btsM1j6fyu3/xMomRDUcyGuEyDDKfPzC9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nPORT/btsM1j6fyu3/xMomRDUcyGuEyDDKfPzC9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnPORT%2FbtsM1j6fyu3%2FxMomRDUcyGuEyDDKfPzC9k%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;741&quot; height=&quot;285&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;285&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;캐시를 적용한 뒤 1 페이지 조회 테스트를 진행하니 모두 성공했다!(1000만건)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 응답 시간 역시 0.02ms로 매우 빠르다.&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;1~20페이지에 랜덤하게 접근하는 경우를 테스트 해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;335&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pH5SP/btsM23OWkzd/AgMtyos5SjbFQ9r39lgG9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pH5SP/btsM23OWkzd/AgMtyos5SjbFQ9r39lgG9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pH5SP/btsM23OWkzd/AgMtyos5SjbFQ9r39lgG9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpH5SP%2FbtsM23OWkzd%2FAgMtyos5SjbFQ9r39lgG9k%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;731&quot; height=&quot;335&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;335&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;213&quot; data-origin-height=&quot;48&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdy3k9/btsM21jhJGr/uUOT7OM3akLboxWJpdDeq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdy3k9/btsM21jhJGr/uUOT7OM3akLboxWJpdDeq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdy3k9/btsM21jhJGr/uUOT7OM3akLboxWJpdDeq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbdy3k9%2FbtsM21jhJGr%2FuUOT7OM3akLboxWJpdDeq0%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;213&quot; height=&quot;48&quot; data-origin-width=&quot;213&quot; data-origin-height=&quot;48&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐싱이 잘 되었고, 성능도 잘 나왔다!&lt;/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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1459&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNclmG/btsM1qDg6AO/KGl8ZgL0A6TD9jjWDh8GV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNclmG/btsM1qDg6AO/KGl8ZgL0A6TD9jjWDh8GV0/img.png&quot; data-alt=&quot;천만건&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNclmG/btsM1qDg6AO/KGl8ZgL0A6TD9jjWDh8GV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNclmG%2FbtsM1qDg6AO%2FKGl8ZgL0A6TD9jjWDh8GV0%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;1459&quot; height=&quot;361&quot; data-origin-width=&quot;1459&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;천만건&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 1페이지에 대한 천만건 데이터의 평균 응답이 9.62초였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;285&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nPORT/btsM1j6fyu3/xMomRDUcyGuEyDDKfPzC9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nPORT/btsM1j6fyu3/xMomRDUcyGuEyDDKfPzC9k/img.png&quot; data-alt=&quot;천만건&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nPORT/btsM1j6fyu3/xMomRDUcyGuEyDDKfPzC9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnPORT%2FbtsM1j6fyu3%2FxMomRDUcyGuEyDDKfPzC9k%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;741&quot; height=&quot;285&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;285&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;천만건&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 적용한 뒤 천만건 데이터의 평균 응답이 19 밀리초로 줄었다!&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;개선율 = ((초기 응답 시간 - 개선된 응답 시간) / 초기 응답 시간 ) * 100&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; (9.62 s - 19 ms &amp;nbsp;/ 9.62s) * 100&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 응답 시간이 약 &lt;b&gt;506.32&lt;/b&gt; 배 빨라졌고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선율은 &lt;b&gt;99.80%&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 전체 조회 외에도, 검색 기능이 구현되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 검색 기능은 키워드가 존재하기에 키워드를 인덱스로 적용하면 성능이 좋아지는지도 테스트해 추가해 볼 예정이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category> ️ 내가 다시 볼 것</category>
      <category>성능개선</category>
      <author>전호영</author>
      <guid isPermaLink="true">https://securityinit.tistory.com/259</guid>
      <comments>https://securityinit.tistory.com/259#entry259comment</comments>
      <pubDate>Mon, 31 Mar 2025 02:19:38 +0900</pubDate>
    </item>
  </channel>
</rss>