<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발 블로그</title>
    <link>https://webfirewood.tistory.com/</link>
    <description>웹 개발을 위한 공부자료</description>
    <language>ko</language>
    <pubDate>Fri, 24 Jul 2026 07:35:51 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>북항</managingEditor>
    <item>
      <title>항해에 나서지 못한 배를 위한 변명</title>
      <link>https://webfirewood.tistory.com/161</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프롤로그: 출항하지 못한 배를 위한 변명&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 프로젝트는 단순한 과업이 아니라, 하나의 세계를 창조하는 일과 같다. 백지 위에 도시를 계획하고, 강줄기의 방향을 정하며, 보이지 않는 곳에 전기와 수도관을 묻는다. 개발자에게 코드란 그 세계를 지탱하는 물리 법칙이자, 사용자들이 거닐게 될 거리의 이름이다. 내가 몸담았던 'AI 디지털 교과서' 프로젝트는 그런 의미에서 한 국가의 미래 세대를 위한 새로운 대륙을 발견하는 일과 다르지 않았다. 낡은 칠판과 분필 가루의 시대를 넘어, 모든 학생이 자신만의 속도와 지도를 가지고 지식의 바다를 항해하게 하자는 원대한 비전. 그 비전의 무게는 개발자의 어깨에 묵직한 사명감으로 내려앉았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 거대한 항해의 시작점에서 나는, 솔직히 말해, 외딴섬에 가까웠다. 이전 팀에서 나는 유능함보다는 유별남으로 알려졌고, 물리적으로도, 심리적으로도 동료들과 멀리 떨어진 채 나만의 작은 프로젝트에 몰두하고 있었다. 스스로를 고립시키며 이직을 고민하던 어느 날, 회사의 '드림팀'이라 불리는 신사업부로의 발령은 난파 직전의 내게 던져진 구원의 동아줄이자, 새로운 대륙으로 떠나는 탐험선의 승선권처럼 느껴졌다. 어둠침침한 레거시 시스템의 기관실을 벗어나, 반짝이는 새 배의 갑판에 올라서게 된 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 모든 위대한 서사에는 회의론자라는 감초가 빠지지 않는 법이다. 우리의 배가 출항 준비를 하는 동안, 항구에는 여러 종류의 수군거림이 떠다녔다. 아이들을 스마트 기기라는 판도라의 상자로부터 지키려는 부모들의 걱정 어린 눈빛, 그리고 교육의 본질은 기술이 아닌 인간의 상호작용에 있다며 학습 효과의 저하를 우려하는 전문가들의 날카로운 지적이 그것이다. 나 역시 그들의 비판이 타당한 근거를 갖고 있음을 부정하지 않았다. 기술은 만병통치약이 아니며, 때로는 득보다 실이 많을 수 있다는 사실을 알고 있었기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;더 위험한 것은 눈에 보이는 파도가 아니었다. 우리의 항해는 순수한 기술 탐사가 아니라, '정부의 공약'이라는 깃발을 높이 내건, 고도의 정치적 원정이었다. 이는 정권이라는 바람의 방향이 바뀌거나, 국회라는 예측 불가능한 해협을 통과하지 못하면 언제든 좌초될 수 있음을 의미했다. 훗날 현실이 된 이 불길한 예감은, 당시에는 그저 안개 속에 희미하게 보이는 위협 정도로만 여겨졌다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 결국 항구에 영원히 정박하게 된 그 배에 대한 기록이다. 배의 용골(아키텍처)은 어떻게 세워졌고, 엔진(핵심 기술)은 어떤 원리로 힘차게 고동쳤으며, 예측 불가능한 돌발 변수(요구사항 변경)와 거친 파도(기술적 난제)를 어떻게 헤쳐 나갔는지에 대한 기술적 항해 일지다. 동시에 이는 완벽하게 건조된 배가 왜 출항조차 하지 못했는지에 대한 해부학 보고서이기도 하다. 비록 우리의 배는 신대륙의 흙을 밟지 못했지만, 그 과정에서 우리가 그린 항해도는 기술이라는 나침반과 현실이라는 지도 사이에서 길을 찾는 또 다른 항해자들에게 의미 있는 참고자료가 되리라 믿는다. 이것은 실패에 대한 구차한 변명이 아니라, 실패를 통해 얻은 값진 교훈에 대한 겸허한 고백이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1장: 백지 위에 그리는 설계도 &amp;ndash; 모놀리식과 MSA 사이의 줄다리기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;새로운 프로젝트의 시작은 창세기의 첫 장과도 같다. 태초에 혼돈과 공허뿐이었듯, 내 앞에는 거대한 백지와 '전국의 모든 학생을 위한 AI 기반 교육 플랫폼'이라는 막막한 목표만이 놓여 있었다. 나를 이끌어 줄 경험 많은 항해사, 즉 직속 시니어는 정부 관계자 및 외부 협력사들과의 외교전이라는 더 큰 파도를 막기 위해 선교(船橋)에서 내려올 틈이 없었다. 결국 배의 심장부인 기관실에는 이제 막 견습을 마친 패기 넘치는 후배 개발자와 나, 단둘이 남겨졌다. 우리는 이 거대한 배의 뼈대를 어떻게 세울지, 동력은 어디서 얻을지, 수많은 구획을 어떻게 나눌지를 결정해야 했다. 이는 갓 면허를 딴 운전사에게 대륙 횡단 트레일러의 열쇠를 건넨 것과 같은, 아찔한 책임감과 동시에 평생에 한 번 올까 말까 한 기회였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;소프트웨어 아키텍처를 설계하는 일은 도시 계획과 유사하다. 한번 도로를 내고 상하수도관을 묻으면, 나중에 도시가 아무리 커져도 그 기본 골격을 바꾸기란 거의 불가능하다. 우리가 내리는 첫 결정은 앞으로 수많은 개발자가 따르게 될 일종의 헌법 조항과도 같았기에, 나는 신중해야만 했다. 가장 먼저 부딪힌 근본적인 질문은 '우리는 어떤 형태의 도시를 건설할 것인가?'였다. 이는 기술의 언어로 '모놀리식(Monolithic)으로 갈 것인가, 마이크로서비스(MSA, Microservice Architecture)로 갈 것인가'의 문제로 번역된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&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;1280&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Uoy0F/btsPB6RuFWU/wQ2qlZdUqMJjbckxLGJfKK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Uoy0F/btsPB6RuFWU/wQ2qlZdUqMJjbckxLGJfKK/img.jpg&quot; data-alt=&quot;모놀리식 아키텍처는 뉴욕의 그랜드 센트럴 터미널과 같다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Uoy0F/btsPB6RuFWU/wQ2qlZdUqMJjbckxLGJfKK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUoy0F%2FbtsPB6RuFWU%2FwQ2qlZdUqMJjbckxLGJfKK%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;1280&quot; height=&quot;853&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모놀리식 아키텍처는 뉴욕의 그랜드 센트럴 터미널과 같다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 세상에는 다른 종류의 건축 철학도 존재했다. 바로 MSA, 마이크로서비스 아키텍처다. 이는 하나의 거대 건물을 짓는 대신, 각기 다른 기능을 가진 작은 전문 건물들로 이루어진 현대적인 도시를 건설하는 방식과 같다. 금융가, 쇼핑 지구, 주거 단지가 독립적으로 존재하되, 잘 닦인 도로와 지하철(API)을 통해 서로 통신하는 모습이다. 이 도시의 가장 큰 장점은 유연성과 확장성이다. 쇼핑 지구가 붐빈다고 해서 도시 전체를 재건축할 필요 없이, 쇼핑몰만 몇 개 더 지으면 그만이다. 한 건물에 불이 나도 도시 전체가 마비되지 않는다는 '장애 격리'의 미덕도 갖추고 있다. 미래의 불확실한 요구사항에 대응하고, 각 서비스 팀이 독립적으로 빠르게 움직일 수 있다는 점에서 MSA는 의심할 여지없이 매력적인 최신 트렌드였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;문제는 이 프로젝트의 발주서에 있었다. 고객, 즉 정부 부처의 요구사항 명세서에는 'MSA 구축'이라는 단어가 마치 종교적 신념처럼 굵은 글씨로 박혀 있었다. 기술적 합리성이나 효율성에 대한 논의는 끼어들 틈이 없었다. 그것은 선택지가 아니라, 반드시 따라야 할 계시와도 같았다. 나는 마치 최고의 프랑스 요리사에게 세상에서 가장 섬세한 수플레를 만들되, 도구는 오직 이 커다란 쇠망치만 사용해야 한다는 주문을 받은 듯한 기분이었다. 기술자의 양심은 모놀리식이라는 잘 벼려진 칼을 가리키고 있었지만, 비즈니스라는 거대한 힘은 MSA라는 낯설고 복잡한 조립식 도구 세트를 강요하고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;결국, 우리는 외줄타기를 시작해야 했다. 기술적 신념과 비즈니스 현실이라는 두 개의 절벽 사이에서 균형을 잡아야 하는 것, 어쩌면 이것이 모든 아키텍트의 숙명일지도 모른다. 우리는 MSA라는 목적지를 향해 돛을 올리기로 결정했다. 하지만 이 결정은 맹목적인 추종이 아니었다. '만약 우리가 MSA라는 도시를 건설해야만 한다면, 혼란스러운 난개발이 아닌, 명확한 구획과 원칙을 가진 계획도시를 만들자.' 이것이 우리의 새로운 목표가 되었다. 나는 도메인 주도 설계(DDD)라는 도시 계획 이론을 나침반 삼아, 각 서비스라는 구역의 경계를 명확히 긋고, 그들이 서로 어떻게 소통하고 협력해야 할지에 대한 상세한 도시 조례를 만들기 시작했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 나의 백지는, 순수한 기술적 이상향이 아닌, 현실과의 치열한 타협과 고민이 담긴 복잡한 설계도로 서서히 채워져 나갔다. 이것은 항해의 시작에 불과했다. 이제 우리는 이 설계도를 들고, 실제 벽돌을 쌓고 배관을 연결하는 고된 노동의 단계로 나아가야 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2장: 엔진실의 열기 &amp;ndash; CQRS, Kafka, 그리고 창의적 타협&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;본격적인 개발 단계에 접어들자, 엔진실은 뜨거운 열기로 가득 찼다. 우리는 도메인 주도 설계(DDD)라는 정교한 설계 원칙에 따라, 거대한 시스템이라는 선체를 여러 개의 독립된 구획으로 나누는 작업부터 시작했다. 학생의 학습 활동 데이터가 오가는 '교과학습 서비스'는 배의 심장부인 엔진실, 교사의 성적 및 출결 관리를 책임지는 '학습관리(LMS) 서비스'는 모든 것을 통제하는 조타실, 그리고 교과서와 같은 핵심 콘텐츠를 저장하고 배포하는 '콘텐츠 관리 서비스'는 배의 모든 자원을 보관하는 거대한 화물칸과 같았다. 이처럼 각자의 역할이 명확한 작은 배(마이크로서비스)들이 탄생했고, 우리는 이 분산된 함대를 지휘하기 위해 'API 게이트웨이'라는 이름의 관제탑을 세웠다. 모든 외부 선박(클라이언트 요청)은 반드시 이 관제탑을 거쳐야만 했고, 관제탑은 요청의 종류를 파악해 가장 적합한 배로 안내하는 역할을 맡았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;명령과 조회, 그 신성한 분리 (CQRS)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;엔진실이 한창 뜨겁게 달아오르던 어느 날, 갑판 위에서 쩌렁쩌렁한 확성기 소리가 들려왔다. &quot;고용량 트래픽 처리 인증을 받아야 한다!&quot;는, 마치 전시 동원령과도 같은 갑작스러운 명령이었다. 사업의 성패를 결정짓는 상부에서, 최종 평가를 통과하기 위한 기술적 증명이 필요했던 모양이다. 기술적 관점에서 이는 명백한 오버 엔지니어링이었다. 아직 승객도 태우지 않은 유람선에 항공모함급 엔진을 달라는 격이었으니까. 하지만 이 불합리해 보이는 요구는, 역설적으로 우리가 설계한 아키텍처의 기술적 깊이를 증명할 절호의 기회이기도 했다. 여기서 우리는 숨겨두었던 비장의 무기, &lt;b&gt;CQRS(Command Query Responsibility Segregation, 명령과 조회의 책임 분리)&lt;/b&gt; 패턴을 꺼내 들었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;CQRS를 이해하기 위해 잠시 분주한 레스토랑 주방을 상상해보자. 전통적인 시스템은 한 명의 만능 요리사가 주문을 받고(쓰기, Command), 요리법을 찾아보며(읽기, Query), 실제 요리를 하고, 완성된 음식을 내어주는 모든 일을 처리하는 방식과 같다. 손님이 한두 명일 때는 문제가 없지만, 저녁 피크타임이 되면 이 만능 요리사는 병목 현상의 주범이 된다. 주문을 받느라 요리를 못 하고, 요리를 하느라 다음 주문을 받지 못하는 아수라장이 펼쳐진다. CQRS는 이 주방의 역할을 극적으로 분리하는 혁신적인 아이디어다. '주문 받기'라는 쓰기(Command) 작업은 전적으로 카운터의 점원에게 맡기고, '요리하기'와 관련된 읽기(Query) 작업, 즉 주문 확인과 조리 등은 주방 안의 요리사들만 담당하게 하는 것이다.&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;1280&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bilub3/btsPEucaZ0W/wwr3KHg9wzlvQQXf0MibXk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bilub3/btsPEucaZ0W/wwr3KHg9wzlvQQXf0MibXk/img.jpg&quot; data-alt=&quot;주방 안의 요리사들은 직접 주문을 받지 않는다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bilub3/btsPEucaZ0W/wwr3KHg9wzlvQQXf0MibXk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbilub3%2FbtsPEucaZ0W%2Fwwr3KHg9wzlvQQXf0MibXk%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;1280&quot; height=&quot;853&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주방 안의 요리사들은 직접 주문을 받지 않는다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 이 원리를 PostgreSQL 데이터베이스에 적용했다. 모든 데이터 변경 작업, 즉 학생의 답안 제출이나 교사의 평가 입력 같은 '쓰기(Command)' 요청은 오직 하나의 견고한 '주방장 DB(Primary DB)'에서만 처리하도록 했다. 반면, 수많은 학생과 교사가 동시에 데이터를 조회하는 '읽기(Query)' 요청은, 주방장 DB의 데이터를 실시간으로 복제한 여러 개의 '보조 요리사 DB(Replica DBs)'가 나누어 처리하도록 했다. 이로써 답안지를 제출하는 한 명의 학생 때문에, 수천 명의 다른 학생이 문제지를 읽지 못하는 재앙을 원천적으로 차단할 수 있었다. 우리는 성능과 안정성이라는 두 마리 토끼를 모두 잡으며, 갑작스러운 상부의 명령을 기술적 성숙도를 뽐내는 무대로 바꿔버렸다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;멈추지 않는 강, 이벤트의 연대기 (Apache Kafka)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;엔진실 한편에서는 또 다른 종류의 작업이 조용하지만 쉴 새 없이 이루어지고 있었다. 학생의 모든 학습 로그를 기록하거나, 성적 처리가 끝났음을 알리는 알림을 발송하는 일처럼, 굳이 실시간으로 &quot;처리 완료!&quot;를 외칠 필요가 없는 작업들이었다. 이러한 작업들은 즉각적인 응답보다는 '언젠가는 반드시, 그리고 절대로 유실 없이' 처리되는 것이 더 중요했다. 이런 비동기(Asynchronous) 세상의 질서를 잡기 위해, 우리는 &lt;b&gt;Apache Kafka&lt;/b&gt;라는 거대한 중앙 우체국 시스템을 도입했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Kafka를 단순한 우체국이나 메시지 큐(Message Queue)로 생각한다면, 그것은 고래를 커다란 물고기 정도로 여기는 것과 같다. 전통적인 메시지 큐는 편지를 한 번 배달하고 나면 우체통에서 사라지는, 소비 지향적인 모델에 가깝다. 반면, Kafka는 모든 사건(Event)을 시간 순서대로 기록하고 영원히 보관할 수 있는, &lt;b&gt;분산 이벤트 스트리밍 플랫폼(Distributed Event Streaming Platform)&lt;/b&gt; 이다. 그것은 우체국이라기보다, 인류의 모든 역사를 발생 순서대로 기록하는 거대한 연대기 혹은 멈추지 않고 흐르는 강물과 같다. 한번 강에 흘려보낸 것은 사라지지 않고 계속해서 하류로 흘러가며, 누구든 강가에 서서 그 물을 떠다 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Kafka의 세계에서 모든 데이터는 &lt;b&gt;이벤트(Event)&lt;/b&gt; 라는 형태로 존재한다. '오후 2시 10분, 학생 A가 3번 문제를 클릭함'이라는 하나의 사건이 바로 이벤트다. 이러한 이벤트들은 관련 있는 것끼리 묶여 &lt;b&gt;토픽(Topic)&lt;/b&gt; 이라는 이름의 채널 혹은 강줄기를 형성한다. 예를 들어 '학습활동_로그'라는 토픽에는 모든 학생의 클릭, 스크롤, 답안 입력 이벤트들이 차곡차곡 흘러간다. 이 토픽이라는 강줄기는 다시 &lt;b&gt;파티션(Partition)&lt;/b&gt; 이라는 여러 개의 작은 샛강으로 나뉜다. 이는 엄청난 양의 강물이 한꺼번에 밀려올 때, 여러 물길로 나누어 병목 현상 없이 빠르게 흘려보내기 위함이다. 덕분에 Kafka는 초당 수백만 개의 이벤트를 처리할 수 있는 괴물 같은 처리량을 자랑한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이벤트의 생산자(&lt;b&gt;Producer&lt;/b&gt;)는 강 상류에서 물건을 띄워 보내는 사람과 같다. 우리의 '교과학습 서비스'는 학생의 활동이 발생할 때마다 해당 이벤트를 만들어 '학습활동_로그' 토픽으로 흘려보내는 생산자였다. 이벤트의 소비자(&lt;b&gt;Consumer&lt;/b&gt;)는 강 하류에서 필요한 물건을 건져 올리는 사람이다. '학습 분석 AI 서비스'는 이 토픽의 이벤트를 가져가 학생의 학습 패턴을 분석했고, '학부모 알림 서비스'는 특정 이벤트(예: 시험 완료)를 감지하여 알림을 발송하는 소비자였다. 중요한 것은, 소비자가 이벤트를 가져가더라도 강물 속 이벤트는 사라지지 않는다는 점이다. 이는 마치 역사책을 누군가 읽는다고 해서 그 내용이 지워지지 않는 것과 같다. 덕분에 새로운 소비자(예: '오답노트 자동 생성 서비스')가 나중에 생겨나더라도, 처음부터 모든 역사를 다시 읽으며 필요한 작업을 수행할 수 있는 놀라운 유연성을 제공한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식의 가장 큰 아름다움은 &lt;b&gt;'느슨한 결합(Loose Coupling)'&lt;/b&gt; 이라는 개념에 있다. 생산자와 소비자는 서로의 존재를 전혀 알 필요가 없다. 그들은 오직 Kafka라는 강을 매개로 소통할 뿐이다. 특정 소비자 서비스가 잠시 아파서 드러눕거나 시스템 업데이트를 위해 잠시 멈추더라도, 생산자는 아랑곳하지 않고 계속해서 이벤트를 강에 흘려보낸다. 이벤트들은 Kafka라는 안전한 저장소에 차곡차곡 쌓여 있다가, 소비자가 복귀하면 자신이 마지막으로 읽었던 지점부터 다시 이벤트를 처리하기 시작한다. 덕분에 시스템의 일부에 장애가 발생하더라도 전체 시스템이 멈추는 일 없이, 각자의 속도에 맞춰 탄력적으로 운영될 수 있었다. Kafka는 우리 함대 내의 각 함선들이 서로 직접 소리쳐 통신하는 대신, 모두가 공유하는 안전하고 신뢰할 수 있는 방송 채널을 통해 소통하게 만든, 마이크로서비스 아키텍처의 신경계와도 같은 존재였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;데이터로 빚은 유령, 실시간의 재구성 (WebSocket)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;허나 이 모든 기술적 도전 중에서도, 가장 정교한 창의력을 요구했던 과제는 단연 '교사의 학생 화면 실시간 제어' 기능이었다. 기획서에 명시된 이 한 줄의 요구는, 기술적으로 번역하면 '모든 학생의 컴퓨터 화면을 실시간 영상으로 교사에게 스트리밍하라'는, 사실상 불가능에 가까운 요구였다. 이는 교실의 모든 학생에게 개인용 방송 중계차를 한 대씩 붙여주는 것과 같은, 물리적으로 불가능한 수준의 네트워크 자원을 소모하는 일이었다. 수많은 반론과 토론의 소용돌이 속에서, 나는 한 걸음 물러서 문제의 본질을 파고들었다. 교사에게 진정으로 필요한 것은 학생의 마우스 커서 움직임 하나하나를 픽셀 단위로 감시하는 전지전능한 통제(Control)인가, 아니면 학생이 지금 무엇을 하고 있는지, 혹시 학습의 길 위에서 헤매고 있지는 않은지를 파악하는 섬세한 인지(Awareness)인가.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 해답은 '제어'가 아닌 '인지'에 있었다. 이 깨달음은 우리로 하여금 완전히 새로운 길을 고안하게 했다. 우리는 발상을 전환하여, 콘서트 실황을 통째로 스트리밍하는 대신, 악보를 실시간으로 전송하는 방식을 택했다. 이 악보 전송을 위한 특별한 통로가 바로 &lt;b&gt;WebSocket&lt;/b&gt; 기술이었다. 전통적인 웹 통신(HTTP)은 마치 편지를 주고받는 것과 같다. 클라이언트가 서버에게 &quot;이 정보 좀 주세요&quot;라고 요청(Request) 편지를 보내면, 서버는 그에 대한 응답(Response) 편지를 보내고 둘 사이의 연결은 끊어진다. 실시간 채팅처럼 계속해서 대화를 이어가려면, 1초에 수십 번씩 &quot;새로운 메시지 있나요?&quot;라는 편지를 보내야 하는, 매우 비효율적인 방식(Polling)을 사용해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSWUHh/btsPDGqFsaz/apwFutOKOp92YC8KvRPRC0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSWUHh/btsPDGqFsaz/apwFutOKOp92YC8KvRPRC0/img.jpg&quot; data-alt=&quot;콘서트 실황을 실시간 전송하는 대신 악보를 복사해서 나눠 준다면 데이터 비용이 획기적으로 줄어든다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSWUHh/btsPDGqFsaz/apwFutOKOp92YC8KvRPRC0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSWUHh%2FbtsPDGqFsaz%2FapwFutOKOp92YC8KvRPRC0%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;1280&quot; height=&quot;853&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;콘서트 실황을 실시간 전송하는 대신 악보를 복사해서 나눠 준다면 데이터 비용이 획기적으로 줄어든다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;반면, WebSocket은 편지가 아닌, 클라이언트와 서버 사이에 한번 연결되면 계속 열려 있는 &lt;b&gt;전용 전화선&lt;/b&gt;을 개설하는 것과 같다. 이 전화선은 &lt;b&gt;전이중(Full-duplex)&lt;/b&gt; 통신을 지원하여, 양쪽 모두가 원할 때 언제든지 상대방에게 말을 걸 수 있다. 서버는 클라이언트의 요청이 없더라도 새로운 데이터가 생기면 즉시 클라이언트에게 &quot;새 소식이야!&quot;라며 데이터를 밀어 넣어(Push)줄 수 있다. 이 방식은 불필요한 요청-응답의 반복을 없애고, 매우 낮은 지연 시간(Low Latency)으로 실시간에 가까운 데이터 교환을 가능하게 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 이 WebSocket이라는 전화선을 통해, 학생의 화면 픽셀 데이터를 보내는 것이 아니라, 학생의 의미 있는 '행동'&amp;mdash;문제 클릭, 답안 입력, 스크롤 이동 등&amp;mdash;만을 가벼운 텍스트 기반의 JSON 데이터로 구조화하여 전송했다. 교사의 클라이언트는 이렇게 전송된 '악보'를 받아, 학생의 화면을 눈앞에서 그대로 '연주'해내는 정교한 오케스트라의 역할을 수행했다. '오후 3시 15분, 학생 B가 5번 문제의 빈칸에 숫자 45 입력'이라는 JSON 데이터는 단순한 텍스트 로그가 아니라, 교사 화면의 5번 문제 빈칸에 '45'라는 숫자를 그려 넣으라는 명확한 지시(Instruction)로 해석되었다. 그 결과, 교사는 막대한 자원 소모 없이도 학생의 화면을 데이터로 재구성된 유령(a data-driven doppelg&amp;auml;nger)처럼 실시간으로 목도할 수 있게 된 것이다. 이 창의적 타협은 불가능의 영역에 있던 비즈니스 요구사항과 기술적 현실의 간극을 메운, 우리 팀의 지성이 가장 빛났던 순간으로 기록될 만했다. 그렇게 엔진실의 열기는 단순한 기계의 소음을 넘어, 복잡한 문제들을 풀어내는 지성의 협주곡으로 승화하고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3장: 갑판 위의 사람들 &amp;ndash; 데이터라는 공용어, 문서라는 유산&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;모든 공학적 도전의 이면에는 언제나 인간이라는 가장 예측 불가능한 변수가 존재한다. 견고한 강철도 지치지 않는 증기기관도 결국은 그것을 다루는 사람의 손에 의해 그 운명이 결정되기 때문이다. 기술만큼이나, 어쩌면 그보다 더 지난했던 것은 바로 갑판 위의 사람들과의 관계였다. 프로젝트라는 배가 순항하기 시작할 무렵, 우리 배에는 나보다 10년은 족히 많은 바닷바람을 맞으며 잔뼈가 굵은 베테랑 선원들, 즉 시니어 프리랜서 개발자들이 다수 합류했다. 젊은 항해사가 그린 설계도를 들고 그들에게 다가가 때로는 방향을 제시하고 업무를 요청해야 하는 상황은, 마치 갓 임관한 초임 장교가 백전노장들로 가득한 부대를 지휘해야 하는 것과 같은 아득한 심리적 해협을 건너는 일이었다. 나의 서툰 소통 방식과 증명해야 한다는 조바심에서 비롯된 의욕 과잉이 때로는 오해를 사기도 했고, 이전 조직에서 느꼈던 고립의 망령이 희미하게 되살아나는 듯한 순간도 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 아슬아슬한 관계의 외줄 위에서 나를 구원한 것은 화려한 언변이나 직급이 주는 공허한 권위가 아니었다. 그것은 바로 누구도 부정할 수 없는 객관적 실체, &lt;b&gt;데이터와 문서&lt;/b&gt;였다. 나는 &quot;코드는 끊임없이 변하지만, 잘 쓰인 문서는 역사가 되어 남는다&quot;는 신념을 가지고 있었다. 이 프로젝트에서 문서화는 단순한 기록 행위가 아니었다. 그것은 서로 다른 언어를 사용하는 여러 독립 왕국을 잇는 거대한 외교 시스템이자, 이 복잡한 프로젝트의 유일한 '단일 진실 공급원(Single Source of Truth)'이었다. 우리는 &lt;b&gt;Confluence&lt;/b&gt;를 이 외교 시스템의 수도로 삼았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 수도에서 발행된 문서들은 각기 다른 목적을 가지고 여러 왕국으로 퍼져나갔다. 시스템 아키텍처 다이어그램과 데이터베이스의 ERD는 우리 개발팀이라는 왕국 내에서 통용되는 헌법이자 정밀한 지도였다. 이는 신규 팀원이 빠르게 항로를 파악하게 하는 안내서였고, 베테랑 개발자들과 기술적 합의를 이끌어내는 근거 자료였다. 회의 테이블에서, 나는 나의 짧은 경험을 내세우는 대신 벤치마크 테스트가 보여주는 냉정한 성능 수치를 펼쳐 보였다. &quot;A안은 B안에 비해 응답 시간이 평균 30ms 단축되고, DB 커넥션 사용률을 15% 감소시킵니다&quot;라는 객관적 언어는, 주관적 선호를 넘어선 합리적 의사결정의 기반이 되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 문서들의 진정한 힘은 왕국의 경계를 넘어설 때 발휘되었다. 상세하게 작성된 API 명세서는 '인공지능 연구팀'이라는 이웃 왕국과의 공식적인 통상 조약이었다. 그들은 이 조약문을 통해 우리 시스템에서 어떤 데이터를, 어떤 형식으로 주고받을 수 있는지를 명확히 이해하고, 자신들의 정교한 AI 모델을 우리 배에 매끄럽게 통합할 수 있었다. 서버와 네트워크를 관장하는 '인프라팀'이라는 또 다른 왕국에는, 각 마이크로서비스가 필요로 하는 자원(CPU, 메모리 등)의 요구사항 명세서가 전달되었다. 그들은 이 문서를 바탕으로 각 서비스에 최적화된 땅을 할당하고 길을 내어주었다. 또한, 이 모든 기술 문서는 경영진이라는 상부 조직에 보고하기 위해, 복잡한 공학 언어를 비즈니스 언어로 번역한 세련된 보고서로 재가공되었다. 문서 하나가 때로는 개발 가이드가 되고, 때로는 부서 간 협업의 계약서가 되며, 때로는 프로젝트의 진행 상황을 알리는 공식 성명서의 역할을 했던 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 잘 구축된 문서 시스템은 기술적 리더십의 형태마저 바꾸어 놓았다. 권위는 더 이상 직급이나 목소리의 크기에서 나오지 않았다. 가장 최신의 정보를 담고 있는 문서를 작성하고, 그 논리를 명확하게 설명할 수 있는 사람이 자연스럽게 토론의 중심에 서게 되었다. 데이터라는 반박할 수 없는 사실 앞에서, 우리는 나이와 경력을 넘어 오직 기술적 합리성만을 기준으로 소통하는 진정한 전문가 집단이 될 수 있었다. 기술의 청사진만큼이나 중요한 것은, 그 청사진을 조직의 모든 구성원이 각자의 언어로 이해하고 신뢰하게 만드는 소통의 아키텍처임을, 나는 갑판 위의 사람들을 통해 배우고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;에필로그: 항구에 정박된 배가 남긴 것&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면, 우리의 배는 끝내 출항하지 못했다. 이는 소설의 가장 허무한 결말과도 같았다. 수많은 역경을 헤치고 마침내 최종 목적지를 눈앞에 둔 주인공이, 사소한 실수로 발을 헛디뎌 낭떠러지로 떨어지는 그런 이야기 말이다. 우리는 모든 기술적 요구사항을 완벽하게 구현했다. 치열한 논쟁 끝에 탄생한 CQRS 패턴은 부하 테스트에서 안정적으로 작동했고, Kafka의 이벤트 파이프라인은 단 하나의 데이터 유실 없이 흘러갔으며, WebSocket을 이용한 실시간 화면 공유 기능은 경쟁사들로부터 &quot;가장 기술적으로 뛰어난 결과물&quot;이라는 찬사까지 받았다. 우리의 배는 칠흑 같은 바다를 항해할 준비를 마친, 견고하고 아름다운 강철의 거인이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 배를 띄우는 데 필요한 것은 튼튼한 엔진과 정확한 항해술만이 아니었다. 최종적으로 우리의 발목을 잡은 것은 엔진의 결함이나 설계도의 오류가 아니었다. 그것은 정부 사업 경험 부족으로 인한 '행정 서류 미비'라는, 어이없을 정도로 비기술적인 문제였다. 수천, 수만 줄의 코드로 이루어진 정교한 시스템이, 누군가의 책상 서랍 속에 있어야 할 서류 몇 장 때문에 그 가치를 증명받지 못한 것이다. 그것은 마치 완벽한 교향곡을 작곡하고 모든 악단의 연주 준비를 마쳤지만, 연주회장 대관 신청 서류의 서명이 누락되어 공연이 영원히 취소된 것과 같은 상황이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;정교하게 만든 배가 출항 서류 문제로 항구에 영원히 묶이게 되자, 배 안에서는 혼란이 시작되었다. 실패의 책임이라는 보이지 않는 폭탄은, 그 원인을 제공한 곳이 아닌 가장 연약한 곳에서 터지기 마련이다. 책임의 화살은 보이지 않는 손에 의해 묵묵히 엔진실을 지켰던 개발팀의 몫으로 전가되었고, 한때 '드림팀'이라 불렸던 우리의 배는 해체라는 씁쓸한 결말을 맞았다. 항해의 꿈을 함께 꾸었던 동료들은 모두 회사를 떠나게 되었고, 나 역시 권고 사직이라는 차가운 통보와 함께 쥐꼬리만 한 보상금을 손에 쥐고 텅 빈 부두에 홀로 남겨졌다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;한동안은 분노와 허탈함에 잠 못 이루는 밤이 계속되었다. 내가 쏟아부었던 수많은 밤과 주말, 치열했던 고민과 논쟁의 순간들이 모두 물거품이 되었다는 생각에 사로잡혔다. 그러나 시간이 지나고 폭풍우가 걷히자, 비로소 잔잔한 수면 아래 가라앉아 있던 것들이 그 모습을 드러내기 시작했다. 나는 이번 프로젝트를 통해 아키텍처란 단순히 최신 기술의 목록을 나열하는 행위가 아님을 배웠다. 그것은 비즈니스의 본질과 기술의 한계, 현재의 제약과 미래의 불확실성이라는 서로 다른 힘들이 팽팽하게 맞서는 지점에서, 최적의 균형점을 찾아내는 고도의 의사결정 과정임을 온몸으로 체감했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;또한, 뛰어난 기술력만으로는 결코 성공적인 프로젝트를 이끌 수 없다는 뼈아픈 교훈을 얻었다. 다른 언어를 쓰는 이해관계자들을 설득하고, 명확한 문서로 지식을 공유하며, 신뢰를 기반으로 협업을 이끌어내는 소통의 기술이야말로, 때로는 가장 강력하고 효율적인 알고리즘이 될 수 있음을 깨달았다. 리소스 부족, 예측 불가능한 요구사항, 불합리한 조직 문화, 그리고 프로젝트의 좌초라는 수많은 역경 속에서도, 나는 내가 맡은 기술적 책임을 끝까지 완수했다. 이 경험은 나에게서 자신감을 앗아가는 대신, 어떤 어려운 상황에서도 흔들리지 않고 제 역할을 해내는 단단한 회복탄력성(Resilience)을 선물했다. 실패의 경험이 나의 기술력에 대한 의심이 아닌, 오히려 더 큰 자신감의 원천이 된 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;비록 프로젝트는 좌초되었지만, 그 안에서 피어난 모든 것이 사라진 것은 아니었다. 내가 주도하여 작성했던 수많은 설계 문서와 기술 가이드는 회사의 공식적인 기술 자산(PoC, Proof of Concept)으로 남아, 다음 R&amp;amp;D 사업의 초석이 되었다는 소식을 전해 들었다. 우리의 배는 출항하지 못했지만, 그 배를 만들며 축적된 기술과 경험은 다음 세대의 배를 건조하는 데 쓰일 귀중한 유산으로 남은 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;지금 나는 잠시 항해를 멈추고 뭍에 올라 숨을 고르고 있다. 이따금 항구를 바라보며 우리가 만들었던 그 배를 떠올린다. 그 배는 실패의 상징이 아니라, 내 청춘의 가장 치열했던 고민과 성장이 담긴 기념비다. 나는 안다. 이 씁쓸하지만 귀중했던 항해의 기록이 나의 다음 여정에 든든한 등대가 되어줄 것임을. 그리고 언젠가 다시 새로운 배에 오를 그날, 나는 오늘보다 조금 더 현명하고, 조금 더 단단하며, 기술의 깊이와 사람의 마음을 함께 헤아릴 줄 아는 그런 항해사가 되어 있을 것이다.&lt;/p&gt;</description>
      <category>에세이</category>
      <author>북항</author>
      <guid isPermaLink="true">https://webfirewood.tistory.com/161</guid>
      <comments>https://webfirewood.tistory.com/161#entry161comment</comments>
      <pubDate>Wed, 30 Jul 2025 20:55:26 +0900</pubDate>
    </item>
    <item>
      <title>월 3만 원짜리 팀원과 일의 미래에 대한 기묘한 통찰</title>
      <link>https://webfirewood.tistory.com/160</link>
      <description>&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;서론: 소크라테스적 거래&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;모든 이야기가 항상 거창한 계기에서 시작되는 것은 아니다. 때로는 인류의 역사만큼이나 오래되고 사소한, 그러나 지극히 절박한 결핍이 이야기의 물꼬를 튼다. 21세기를 사는 한 평범한 백수였던 나의 경우가 바로 그랬다. 오랜 직장 생활에 마침표를 찍고 만끽하던 자유의 시간은, 통장 잔고가 줄어드는 속도와 정확히 반비례하며 그 빛을 잃어가고 있었다. 자유라는 고상한 가치는 매달 빠져나가는 월세와 보험료 앞에서 속수무책이었고, 그중에서도 가장 먼저 타격을 입은 것은 인류의 가장 오래된 과업 중 하나인 구애 활동, 즉 데이트 비용이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;어떻게 하면 최소한의 비용으로 최대한 그럴듯한 주말을 보낼 수 있을까?&amp;rsquo; 이 숭고하고도 처절한 질문에 대한 답을 찾아 인터넷을 헤매던 내 눈에, 운명처럼 한 줄기 빛이 들어왔다. 바로 공공데이터포털에서 제공하는 &amp;lsquo;전국 문화행사 정보 API&amp;rsquo;였다. 유료는 물론 무료 행사 정보까지 담긴 이 디지털 창고는 내게 단순한 데이터 목록이 아니었다. 그것은 가뭄에 시달리던 농부가 발견한 마르지 않는 샘이었고, 암흑 속에서 발견한 성냥 한 개비였으며, 절박한 백수에게 주어진 가능성의 불씨였다. &amp;ldquo;이걸로 사람들이 쓸 만한 서비스를 만들어볼까?&amp;rdquo;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;문제는 명확했다. 나는 평생 사용자의 눈에 보이지 않는 땅속 세계, 즉 서버와 데이터베이스를 다루는 백엔드 개발자로 살아왔다. 사용자의 눈에 직접 보이는 화면을 그리고 색칠하는 프론트엔드 개발은 내게 에스페란토어나 고대 수메르어처럼 낯선 미지의 영역이었다. 하지만 곧이어 한층 더 무모한 생각이 뒤따랐다. &amp;lsquo;요즘 그렇게 유행이라던 인공지능(AI)한테 시키면 되지 않을까?'.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;당시 내 마음속에는 AI에 대한 막연한 두려움과 거부할 수 없는 호기심이 뒤섞여 있었다. 실무에서 보조 도구로 AI를 써보긴 했지만, 그것은 마치 잘 훈련된 앵무새에게 몇 마디 말을 가르치는 수준에 불과했다. 나는 이 새로운 지능의 한계가 어디까지인지, 얼마나 멍청하고 또 얼마나 똑똑한지 제대로 시험해보고 싶었다. 이 프로젝트는 단순히 데이트 비용을 아끼기 위한 방편을 넘어, 이 새롭고 이질적인 존재와 협력하여 무언가를 창조하는 것이 어떤 느낌인지 그 본질을 이해하려는 개인적인 탐구로 발전했다. 그렇게 나는 돈 몇 푼을 아끼려다, 월 3만 원짜리 AI 팀원들을 이끄는 어설픈 팀장이 되어버렸다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 지극히 개인적인 탐구는 하나의 강력한 역사적 유사성과 맞닿아 있다. 인류는 언제나 자신의 발명품과 기묘한 애증 관계를 맺어왔다. 우리는 문제를 해결하기 위해 도구를 만들지만, 그 도구는 이내 우리 자신을 바꾸어 놓는다. 망치는 손의 쓰임새를 바꾸고, 자동차는 공간의 개념을 바꾸며, 책은 생각의 구조를 바꾼다. 그리고 지금의 AI처럼, 과거에도 그 시대를 뒤흔든 신기술에 대한 깊은 우려가 존재했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;대표적인 예가 플라톤의 『파이드로스』에 기록된 소크라테스의 글쓰기에 대한 비판이다. 자신의 시대에 등장한 혁신 기술이었던 &amp;lsquo;글쓰기&amp;rsquo;가 진정한 지혜가 아닌 지혜의 &amp;lsquo;외양&amp;rsquo;만을 낳을 것이라 소크라테스는 우려했다. 그는 글쓰기가 기억력을 쇠퇴시키고, 사람들이 내면의 자원을 통해 스스로 기억해내는 대신 외부의 표식에 의존하게 만들 것이라고 걱정했다. 그에게 글쓰기는 &amp;ldquo;기억의 비약이 아니라 상기의 비약(elixir not of memory, but of reminding)&amp;rdquo;이었다. 이 새로운 발명품은 사용자들에게 &quot;가르침 없이 많은 것을 듣게 하여, 그들이 대부분 무지하면서도 많은 것을 아는 것처럼 보이게&quot; 만들고, 결국 &quot;지혜로워 보이는 탓에 함께하기 어려운 사람&quot;으로 만들 것이라고 예언했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;소크라테스의 고대적 근심에서 오늘날 개발자가 AI 코드 어시스턴트를 대하는 경험으로 직접적인 선을 그을 수 있다. 순식간에 코드 조각과 해결책을 생성해내는 이 도구들은 궁극의 &amp;ldquo;상기의 비약&amp;rdquo;이다. 이것들은 우리의 지적 &amp;ldquo;기억&amp;rdquo;, 즉 프로그래밍 원리와 문제 해결 패턴에 대한 깊고 내재화된 이해를 약화시킬 위험이 있다. 하지만 소크라테스의 비판은 여기서 그치지 않는다. 그의 더 깊은 우려는 글쓰기가 상호작용이 불가능한 '죽은 글자'라는 점에 있었다. 글은 &quot;살아있는 존재처럼 서 있지만, 질문을 던지면 장엄한 침묵을 지킬 뿐&quot;이며, &quot;항상 똑같은 한 가지만을 말한다&quot;. 진정한 앎(episteme)은 살아있는 대화, 즉 질문과 답변이 오가는 변증법을 통해서만 얻어지는데, 글은 이러한 역동적인 과정을 원천적으로 차단한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;841&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMPrJm/btsPBIvD4hq/2T1PDhKFPle6T0WkewRnn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMPrJm/btsPBIvD4hq/2T1PDhKFPle6T0WkewRnn1/img.png&quot; data-alt=&quot;소크라테스가 느낀 근심은 오늘날 AI 를 사용하는 개발자들에게 놀랍도록 유사하게 적용된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMPrJm/btsPBIvD4hq/2T1PDhKFPle6T0WkewRnn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMPrJm%2FbtsPBIvD4hq%2F2T1PDhKFPle6T0WkewRnn1%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;1280&quot; height=&quot;841&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;841&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;소크라테스가 느낀 근심은 오늘날 AI 를 사용하는 개발자들에게 놀랍도록 유사하게 적용된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;바로 이 지점에서, 내가 마주한 AI의 본질이 더욱 선명해진다. 나는 이 월 3만 원짜리 디지털 동료를 고용함으로써 소크라테스적 거래를 하고 있는 것인가? 생산성의 외양을 얻는 대가로, 진정한 이해에 도달하는 고통스럽지만 성스러운 과정을 내주고 있는가? 이 에세이는 그 질문에 대한 답을 찾아가는 한 개발자의 서투르고, 때로는 코믹하며, 종종 섬뜩하기까지 한 분투의 기록이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;1부: 디지털 견습생 길들이기&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나의 첫 프로젝트 &amp;lsquo;무료한 문화생활&amp;rsquo;은 AI라는 요술 램프를 처음으로 문질러 본 경험과 같았다. 소원을 들어줄 강력한 지니는 분명 나타났지만, 정작 소원을 명확하게 말하는 법을 몰랐던 주인에게 돌아온 것은 기대와는 조금 다른, 어딘가 기묘한 결과물이었다. 나는 갓 채용한 신입사원에게 일을 맡기는, 다소 거만한 팀장의 태도로 ChatGPT에게 지시를 내렸다. &amp;ldquo;공공데이터 API로 데이트 비용 아낄 서비스 만들고 싶어. 알아서 잘, 그리고 예쁘게.&amp;rdquo; 마치 레스토랑에서 셰프에게 &quot;맛있는 걸로 아무거나 주세요&quot;라고 말하는 것과 같은, 무책임하고도 막연한 주문이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;놀랍게도 AI는 제법 그럴듯한 기획안을 내놓았다. 심지어 별도의 서버 없이 &amp;lsquo;깃헙 액션(Github Action)&amp;rsquo;이라는 자동화 도구를 써서 주기적으로 데이터를 업데이트하라는, 기술적으로 꽤 영리한 제안까지 덧붙였다. 나는 잠시 우쭐해졌다. &amp;lsquo;월 3만 원짜리치고는 꽤 쓸만한데?&amp;rsquo; 이 신입사원의 잠재력에 대한 섣부른 기대감에 부풀어, 나는 디자인과 개발까지 한 번에 맡기는 대담한 결정을 내렸다. 그리고 몇 분 뒤, 내 눈앞에 나타난 결과물은 한마디로 &amp;lsquo;디지털 프랑켄슈타인&amp;rsquo;이었다. 2000년대 초반, 인터넷의 여명기에나 볼 수 있었던 현란한 그라데이션과 의미를 알 수 없는 그림자 효과, 서로 다른 행성에서 온 듯 전혀 어울리지 않는 폰트들의 조합은 차마 눈 뜨고 보기 힘든 수준이었다. 그것은 웹사이트라기보다는, 누군가 포토샵 필터를 홧김에 쏟아부은 뒤 그대로 굳어버린 디지털 참사에 가까웠다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 장엄한 실패는 내게 중요한 교훈을 남겼다. AI에게 한 번에 모든 것을 맡기는 것은, 오케스트라 지휘자가 모든 연주자에게 각자 다른 악보를 나눠주며 &amp;ldquo;자, 이제 알아서 멋진 곡을 연주해봐&amp;rdquo;라고 말하는 것과 같았다. 조화로운 결과물을 위해서는 명확한 역할 분담과 구체적인 지시, 그리고 무엇보다 지휘자의 뚜렷한 비전이 필수적이었다. 나는 접근 방식을 전면 수정했다. 나 자신을 단순한 아이디어 제공자에서 프로젝트의 전체 그림을 그리고 조율하는 &amp;lsquo;팀장&amp;rsquo;으로 재정의하고, 거대한 단일 AI에게 모든 것을 맡기는 대신 기능별로 세분화된 가상의 팀을 꾸렸다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;첫째, 프로젝트의 방향과 사용자 경험의 뼈대를 잡는 &amp;lsquo;기획자 AI&amp;rsquo;. 둘째, 눈에 보이는 모든 것을 책임지는 까다로운 취향의 &amp;lsquo;디자이너 AI&amp;rsquo;. 셋째, 기획과 디자인을 현실로 구현하는 우직한 &amp;lsquo;개발자 AI&amp;rsquo;. 이렇게 역할을 나누자 마법 같은 변화가 일어났다. 나는 더 이상 AI에게 &amp;ldquo;알아서 잘해줘&amp;rdquo;라고 말하지 않았다. 대신 &amp;lsquo;기획자 AI&amp;rsquo;와는 서비스의 핵심 가치에 대해 논쟁했고, &amp;lsquo;디자이너 AI&amp;rsquo;에게는 &amp;ldquo;좀 더 미니멀한 스타일로 가되, 사용자가 가장 중요하게 생각할 무료 행사 정보는 눈에 잘 띄도록 강조해줘. 애플 웹사이트 같은 느낌, 뭔지 알지?&amp;rdquo;라며 구체적인 레퍼런스를 제시했다. 그리고 이 모든 논의를 거친 명세서를 &amp;lsquo;개발자 AI&amp;rsquo;에게 넘겨주자, 비로소 내가 상상했던 그림과 유사한 결과물이 나오기 시작했다. 이 전략적 변화는 내 작은 프로젝트들의 성공률을 극적으로 높였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 깨달음 이후, 나는 마치 컨베이어 벨트에서 제품을 찍어내듯 서비스를 만들기 시작했다. 낯선 사람과의 대화에서 화제가 고갈되는 개인적인 고충을 해결하기 위한 &amp;lsquo;대화주제 생성기&amp;rsquo;, 수익화라는 원대한 꿈을 꾸며 만든 제휴 마케팅 사이트 &amp;lsquo;알요미&amp;rsquo;. 아이디어가 떠오른 지 몇 시간이면 그럭저럭 작동하는 웹사이트 하나가 뚝딱 탄생했다. 이 과정에서 내가 느낀 강렬한 &amp;lsquo;재미&amp;rsquo;와 무시무시한 속도감의 정체는 바로 &amp;lsquo;압축된 피드백 루프(Compressed Feedback Loop)&amp;rsquo;의 힘이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;전통적인 개발 과정은 아이디어 구상에서 실제 작동하는 결과물을 보기까지 기나긴 인내의 시간이 필요하다. 그것은 마치 씨앗을 심고 싹이 트기를, 열매가 맺히기를 기다리는 농부의 시간과 같다. 코드를 한 줄 쓰고, 저장하고, 컴파일하고, 수많은 에러와 싸우고, 마침내 브라우저에서 결과를 확인하기까지의 과정은 수많은 마찰과 지연으로 가득 차 있다. 하지만 AI와의 협업은 이 모든 중간 과정을 증발시켰다. 아이디어를 프롬프트라는 형태로 입력하면, 몇 분 안에 살아 움직이는 코드가 눈앞에 나타났다. 생각과 결과 사이의 거리가 거의 제로에 가까워진 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;심리학에서 피드백 루프는 행동이 환경으로부터 받는 반응에 의해 형성되는 순환 과정을 의미한다. 이 순환이 빠르고 명확할수록 행동은 더 효과적으로 강화되거나 수정된다. AI는 코딩 과정에서 가장 지루하고 마찰이 큰 부분, 예컨대 CSS 스타일을 맞추기 위해 픽셀 단위로 숫자를 조정하거나 반복적인 함수를 작성하는 일을 대신 처리해주었다. 그러자 나는 창작의 가장 핵심적이고 즐거운 부분, 즉 &amp;lsquo;이걸로 뭘 만들까?&amp;rsquo;와 &amp;lsquo;어떻게 다르게 만들어볼까?&amp;rsquo;라는 질문에만 온전히 집중할 수 있었다. 지루한 반복 작업이 사라지고 즉각적인 보상이 주어지자, &amp;lsquo;일&amp;rsquo;은 마치 잘 만든 비디오 게임처럼 느껴지기 시작했다. 최근 연구들은 풍부하고 즉각적인 감각 피드백이 주어질 때 사람들이 주관적으로 느끼는 시간이 압축되는 현상을 보고한다. 나의 경험은 이와 정확히 일치했다. 몇 시간이 순식간에 지나갔고, 나는 창작 행위 자체에 완전히 몰입했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 강력한 쾌감의 이면에는 미묘한 경고등이 깜빡이고 있었다. 즉각적인 보상으로 작동하는 이 피드백 루프는 긍정적 습관을 형성하는 강력한 도구인 동시에, 소셜 미디어의 &amp;lsquo;무한 스크롤&amp;rsquo;이나 유튜브의 &amp;lsquo;자동 재생&amp;rsquo;처럼 우리의 의지력을 좀먹는 파괴적인 습관을 강화하는 메커니즘이기도 하다. 내가 경험한 강렬한 즐거움은 순수한 창작의 기쁨이었을까, 아니면 즉각적인 보상에 길들여진 뇌가 만들어내는 도파민의 향연이었을까? 이 압축된 피드백 루프가 주는 달콤함이, 어쩌면 더 깊고 느린 사유가 필요한 어려운 문제에 대한 나의 인내심을 갉아먹고 있는 것은 아닐까 하는 의문이 고개를 들기 시작했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나의 짧은 개발 여정의 정점은, 내가 전혀 알지 못하는 미지의 영역에 무모하게 발을 들였을 때 찾아왔다. AI와 이런저런 아이디어를 던지며 브레인스토밍을 하던 중, &amp;lsquo;AI 얼굴 나이 맞히기&amp;rsquo;라는 아이디어가 눈에 띄었다. &amp;lsquo;서버 없이 브라우저에서만 이게 가능하다고?&amp;rsquo; 회의적인 내 질문에 AI는 `face-api.js`라는 자바스크립트 라이브러리의 존재를 알려주었다. 나는 스스로를 자책했다. 수년간 서버만 들여다본 탓에, 세상이 얼마나 변했는지 까맣게 모르고 있었던 것이다. 전문 개발자라는 사람이 이런 강력한 기술의 존재조차 모르고 있었다니. 구글링을 해보니 `face-api.js`는 이미 2018년경에 등장했고, 그와 유사한 기술을 활용한 서비스들은 내가 이 아이디어를 떠올리기 한참 전부터 인기를 끌고 있었다. 나는 한참 늦어버린 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 부끄러운 발견은 그러나 기묘한 연쇄 반응을 일으켰다. `face-api.js`가 단순히 얼굴을 인식하는 것을 넘어 눈, 코, 입의 좌표까지 찾아준다는 사실을 알게 된 나는 AI에게 다시 물었다. &amp;ldquo;이 좌표로 또 뭘 할 수 있을까?&amp;rdquo; AI는 &amp;lsquo;퍼스널 컬러 진단&amp;rsquo;을 제안했다. 솔직히 나는 퍼스널 컬러가 무엇인지도 몰랐다. &amp;lsquo;웜톤&amp;rsquo;, &amp;lsquo;쿨톤&amp;rsquo; 같은 단어는 들어봤지만, 그것이 화장품 가게의 상술인지 과학적 근거가 있는 이론인지조차 알지 못했다. AI가 그 원리를 장황하게 설명해주었지만, 색채 이론과 이미지 처리 기술이 뒤섞인 설명은 내 머릿속에 들어오지 않았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;바로 그 순간, 나는 대담하고도 지극히 게으른 결정을 내렸다. 내 안의 책임감 있는 엔지니어는 &amp;lsquo;이해하지 못하는 기술을 사용해서는 안 된다&amp;rsquo;고 속삭였지만, 호기심 많은 게으름뱅이는 &amp;lsquo;그래서 결과가 어떻게 되는데?&amp;rsquo;라고 부추겼다. 결국 게으름뱅이가 이겼다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;모르겠고, 그냥 만들어 줘.&amp;rdquo;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;기획, 디자인, 개발. 나의 AI 팀원들은 또 한 번 결과물을 뱉어냈다. 나는 반신반의하며 주변 지인 몇 명에게 테스트를 부탁했다. 잠시 후, 놀라운 메시지가 도착했다. &amp;ldquo;이거 어떻게 만들었어? 나보고 &amp;lsquo;가을 웜톤&amp;rsquo;이래. 작년에 전문가한테 돈 주고 진단받은 거랑 똑같아!&amp;rdquo; 소름이 돋았다. 나는 `face-api.js`의 작동 원리도, 퍼스널 컬러 이론의 핵심도, 심지어 내부에 어떤 계산 로직이 돌아가는지도 전혀 모르는데, 그럭저럭 작동하는 결과물이 탄생한 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;바로 이 지점에서 나는 섬뜩한 질문과 정면으로 마주했다. 나는 과연 AI를 내 지식을 증폭시키는 &amp;lsquo;확성기&amp;rsquo;로 활용하고 있는 것인가, 아니면 그저 AI에게 뇌의 일부를 의탁한 채 생각 없이 엔터키만 누르는 &amp;lsquo;디지털 꼭두각시&amp;rsquo;로 전락하고 있는가? 편리함에 기대어 생각하는 과정을 외부에 떠넘기는 &amp;lsquo;인지적 오프로딩(cognitive offloading)&amp;rsquo;의 안락한 함정에 빠져들고 있는 것은 아닐까 하는 불안감이 엄습했다. 이 개념은 우리가 스마트폰에 전화번호를 저장하면서 친구의 번호를 더 이상 외우지 않게 되는 현상을 설명한다. 전문가에게 AI는 강력한 부스터가 될 수 있지만, 나처럼 어설프게 아는 사람에게는 생각하는 능력을 쇠퇴시키는 달콤한 독이 될 수도 있었다. 내가 만든 것은 과연 나의 창작물인가, 아니면 나는 그저 AI가 뱉어낸 결과물을 세상에 내놓는 배달부에 불과한가?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;2부: 기계 속 유령과 양심의 무게&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나의 AI 팀원들은 눈부시게 똑똑했지만, 동시에 심각한 결함을 가진 동료이기도 했다. 이들과의 협업은 마치 안개 낀 숲속에서 유능하지만 변덕스러운 유령과 함께 길을 찾는 여정과 같았다. 그 유령은 놀라운 지름길을 알려주는가 싶다가도, 홀연히 사라지거나 엉뚱한 방향을 가리키며 나를 곤경에 빠뜨리곤 했다. 이 디지털 견습생들은 때때로 너무나도 능숙하게 거짓말을 하고(환각), 예상보다 훨씬 많은 월급을 축내며(토큰 비용), 자신이 내놓은 결과물에 대해서는 일말의 책임감도 느끼지 않았다(윤리적 문제).&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트의 규모가 조금이라도 복잡해지면, 어김없이 기묘한 현상이 발생했다. 프로젝트 초반, 명민하고 총기 넘치던 나의 &amp;lsquo;신입사원&amp;rsquo;은 대화가 길어질수록 점차 총기를 잃고 멍텅구리가 되어갔다. 방금 전에 우리가 합의했던 디자인 시안을 까맣게 잊어버리거나, 프로젝트의 핵심 목표와는 전혀 상관없는 엉뚱한 코드를 제안하기 시작했다. 이것은 단순한 변덕이 아니었다. 문제의 핵심에는 &amp;lsquo;컨텍스트 창(Context Window)&amp;rsquo;이라는, 대규모 언어 모델(LLM)이 가진 근본적인 구조적 한계가 자리 잡고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;AI의 컨텍스트 창은 흔히 &amp;lsquo;작업 기억(working memory)&amp;rsquo;에 비유된다. AI가 한 번의 대화에서 기억하고 처리할 수 있는 정보의 총량에는 물리적인 제한이 있다는 뜻이다. 이는 마치 눈코 뜰 새 없이 바쁜 레스토랑의 베테랑 웨이터와 같다. 그는 방금 들어온 1번 테이블의 주문은 완벽하게 기억하지만, 30분 전에 7번 테이블 손님과 나눴던 스몰토크의 내용은 이미 머릿속에서 지워버린 지 오래다. 대화가 길어져 이 용량을 초과하는 순간, AI는 가장 오래된 정보를 가차 없이 창밖으로 밀어내 버린다. 사람과 사람은 대화가 깊어질수록 서로에 대한 이해와 신뢰가 쌓이지만, 현재의 AI는 역설적이게도 대화가 길어질수록 중요한 맥락을 잃고 &amp;lsquo;바보&amp;rsquo;가 되어가는 것이다. 나는 팀장이 아니라, 5분마다 프로젝트의 처음부터 끝까지 모든 것을 다시 브리핑해야 하는 기억상실증 환자의 간병인이 된 기분이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 아슬아슬한 기술적 한계는 곧바로 지갑을 위협하는 경제적 문제로 이어졌다. AI와의 모든 상호작용은 토큰이라는 화폐를 통해 정산된다. 컨텍스트 창을 가득 채운 긴 질문을 던질 때마다, 내 계좌에서는 마치 택시 미터기처럼 비용이 실시간으로 빠져나갔다. 이 보이지 않는 비용은 나의 창의적인 과정에 미묘하지만 확실한 족쇄를 채웠다. &amp;lsquo;이 아이디어를 시험해볼까?&amp;rsquo; 하는 순수한 호기심이 &amp;lsquo;이 질문은 과연 몇백 원의 가치가 있을까?&amp;rsquo;라는 현실적인 계산 앞에서 주춤거리기 시작했다. 자유로운 브레인스토밍은 어느새 비용 효율을 따지는 조심스러운 탐색으로 변질되었다. 그것은 창의성에 부과된 세금, 호기심에 매겨진 벌금과도 같았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 금전적 비용보다 훨씬 더 무겁게 다가온 것은 바로 윤리적 딜레마의 무게였다. 나의 작은 장난감 프로젝트였던 &amp;lsquo;퍼스널 컬러 진단기&amp;rsquo;는 의도치 않게 나를 불편한 진실과 마주하게 했다. 내가 가볍게 활용했던 `face-api.js` 라이브러리는 단순히 얼굴의 특징점을 찾는 것을 넘어, 사용자의 감정, 성별, 나이까지 추정하는 기능을 품고 있었다. 공교롭게도 이 기능들은 거대 기술 기업인 마이크로소프트가 자사의 서비스에서 공식적으로 폐기하기로 결정했던 바로 그 기술들이었다. 오용될 경우 특정 집단에 대한 고정관념을 강화하고 차별을 야기할 수 있다는 심각한 우려 때문이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나는 순수한 재미로 시작한 내 작은 프로젝트가, 사회적으로 이미 위험성이 경고된 기술의 연장선 위에 놓여있다는 사실을 깨닫고 섬뜩함을 느꼈다. 나는 과연 &amp;lsquo;가을 웜톤&amp;rsquo;이라는 유쾌한 결과 뒤에 숨겨진, 잠재적인 편향의 가능성을 충분히 인지하고 있었는가? 이 모델이 특정 인종이나 성별에 대해 편향된 결과를 내놓을 가능성은 없는가? 여기에 더해, AI가 학습 과정에서 사용했을 수많은 이미지와 코드의 저작권 문제라는 또 다른 법적 지뢰밭이 안개처럼 펼쳐져 있었다. 내가 무심코 사용하고 배포한 AI 생성 코드가, 사실은 특정 라이선스가 걸린 코드를 무단으로 복제한 결과물일 수 있다는 가능성은 이 편리한 기술의 이면에 숨겨진 책임의 무게를 실감하게 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;바로 이 지점에서 우리는 중요한 통찰에 도달한다. 기술적 문제로 치부되는 &amp;lsquo;환각(Hallucination)&amp;rsquo;과 사회적 문제로 여겨지는 &amp;lsquo;편향(Bias)&amp;rsquo;은 사실 별개의 사안이 아니다. 이들은 딥러닝 모델의 본질인 &amp;lsquo;불투명한 통계적 추론&amp;rsquo;이라는 동일한 근원에서 파생된 두 개의 다른 증상일 뿐이다. 따라서 AI 시대 개발자의 역할은 순수하게 기술적인 영역에서 사회-기술적인 영역으로 확장될 수밖에 없으며, 이는 우리에게 새로운 형태의 책임, 즉 &amp;lsquo;윤리적 디버깅(Ethical Debugging)&amp;rsquo;을 요구한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이제 개발자는 더 이상 &amp;ldquo;코드가 기술적으로 작동하는가?&amp;rdquo;라고만 물을 수 없다. 우리는 반드시 &amp;ldquo;이 AI가 생성한 코드에 숨겨진 사회적 가정은 무엇인가?&amp;rdquo;, &amp;ldquo;이 라이브러리가 무의식적으로 영속시킬 수 있는 편향은 무엇인가?&amp;rdquo;라고도 물어야 한다. 디버깅이라는 행위는 이제 코드의 논리적 오류를 찾아 수정하는 것을 넘어, 그 코드가 사회에 미칠 수 있는 윤리적, 사회적 파급 효과를 분석하고 완화하는 과정까지 포함하게 된 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 새롭고 무거운 책임은, 에세이의 서두에서 언급했던 소크라테스가 &amp;lsquo;글쓰기&amp;rsquo;라는 신기술을 비판했던 지점과 정확히 맞닿아 있다. 소크라테스는 글이 &amp;ldquo;자신을 변호할 줄도 모르고, 누구에게 말해야 하고 누구에게 말하지 말아야 할지 알지 못한다&amp;rdquo;고 우려했다. 한번 세상에 나온 글은 그 의도와 상관없이 홀로 떠다니는 &amp;lsquo;고아&amp;rsquo;와 같다는 것이다. AI가 생성한 코드 역시 마찬가지다. 그것은 맥락도, 의도도, 윤리적 분별력도 없이 그저 존재할 뿐인, 소크라테스가 말한 &amp;lsquo;죽은 글자&amp;rsquo;의 완벽한 21세기적 현현(顯現)이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;흥미롭게도 소크라테스의 비판에 대한 해답은, 역설적으로 그 비판을 &amp;lsquo;글로 기록한&amp;rsquo; 제자 플라톤의 행위 속에 암시되어 있다. 소크라테스는 글이 부당하게 비난받을 때 &amp;ldquo;언제나 아버지의 도움이 필요하다&amp;rdquo;고 말했다. 플라톤은 스승 소크라테스의 &amp;lsquo;말&amp;rsquo;이라는 고아에게 &amp;lsquo;글&amp;rsquo;이라는 육신을 부여하고, 자신의 편집과 해석을 통해 그 &amp;lsquo;아버지&amp;rsquo;의 역할을 자처했다. 이처럼 오늘날의 개발자는 AI가 낳은 이 유령 같은 &amp;lsquo;고아 코드(Orphan Code)&amp;rsquo;의 아버지가 되어야만 한다. 생성된 코드를 맹목적으로 복사-붙여넣기 하는 &amp;lsquo;디지털 앵무새&amp;rsquo;가 아니라, 그 코드를 신중하게 입양하여 맥락을 부여하고, 편향을 검증하며, 그것이 세상에 미칠 모든 영향에 대해 전적인 책임을 지는 후견인이 되어야 하는 것이다. 이것이야말로 &amp;lsquo;윤리적 디버깅&amp;rsquo;의 본질이며, AI 시대 개발자에게 주어진 피할 수 없는 플라톤적 과업이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;3부: 사냥의 기묘한 즐거움&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트들을 닥치는 대로 밀어붙이던 어느 늦은 밤, 나는 문득 키보드에서 손을 떼고 의자 깊숙이 몸을 기댔다. 창밖은 칠흑 같은 어둠에 잠겨 있었고, 모니터에서는 방금 완성된 코드가 희미한 빛을 뿜어내고 있었다. 그 순간 나를 덮친 것은 성취감이나 안도감이 아니었다. 그것은 지극히 단순하고도 당혹스러운 질문이었다. &amp;lsquo;어째서 이 모든 과정이 이토록 재미있었을까?&amp;rsquo;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;분명 이것은 &amp;lsquo;일&amp;rsquo;이었다. 데드라인은 없었지만 스스로 부여한 목표가 있었고, 해결해야 할 기술적 문제가 산적해 있었다. 하지만 과정은 노동의 고통보다는 잘 만든 비디오 게임의 몰입감에 가까웠다. 몇 시간은 몇 분처럼 느껴졌고, 식사도 잊은 채 모니터에 빠져들었다. 한때는 그토록 지긋지긋하게 느껴졌던 코딩이라는 행위가, 어째서 AI라는 동료 하나를 들인 것만으로 손에 땀을 쥐게 하는 스포츠 경기처럼 변모했단 말인가? 이 기묘한 즐거움의 정체를 해부해볼 필요가 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 해답의 실마리는 심리학자 미하이 칙센트미하이(Mihaly Csikszentmihalyi)가 평생에 걸쳐 탐구한 &amp;lsquo;몰입(Flow)&amp;rsquo;이라는 개념 속에 있었다. 몰입이란 어떤 활동에 완전히 빠져들어 시간의 흐름이나 자의식마저 잊게 되는 최적의 경험 상태를 말한다. 칙센트미하이에 따르면, 몰입은 몇 가지 조건이 충족될 때 비로소 찾아온다. 명확한 목표, 행위에 대한 즉각적인 피드백, 그리고 가장 중요하게는 당면한 과제의 난이도와 내 능력치가 아슬아슬한 균형을 이룰 때다. 과제가 너무 쉬우면 지루함을 느끼고, 너무 어려우면 불안감에 휩싸인다. 몰입은 그 지루함과 불안함 사이의 좁고 황홀한 협곡을 지날 때 경험하는 최고의 희열이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;돌이켜보면, 나의 AI 신입사원은 이 몰입의 조건들을 완벽하게 조율해주는 일종의 &amp;lsquo;몰입 상태 유도 장치(Flow State Induction Engine)&amp;rsquo;였다. 첫째, &amp;lsquo;무료 문화 행사 정보 사이트 만들기&amp;rsquo; 같은 작고 명확한 목표는 내가 어디로 가야 할지 알려주는 북극성이 되어주었다. 둘째, 1부에서 언급했던 &amp;lsquo;압축된 피드백 루프&amp;rsquo;는 나의 모든 행동에 거의 실시간으로 반응하며 길을 안내하는 영리한 셰르파와 같았다. 하지만 이 엔진의 진짜 마법은 세 번째 조건, 즉 &amp;lsquo;도전과 기술의 균형&amp;rsquo;을 동적으로 조율하는 능력에 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나의 가장 큰 골칫거리는 프론트엔드 개발이었다. CSS의 미세한 픽셀 하나를 조정하다 보면 자신감은 바닥나고 불안감만 차오르곤 했다. 바로 이 지점에서 AI는 구원투수처럼 등판했다. 내가 감당하기 힘든 불안의 영역을 기꺼이 떠맡아 주었다. 반면 내가 어느 정도 자신감을 가진 백엔드 로직 설계나 프로젝트의 전체적인 방향을 설정하는 문제처럼, 적절한 도전의식을 불러일으키는 과업은 고스란히 나의 몫으로 남겨두었다. AI는 마치 내 능력에 맞춰 높낮이를 실시간으로 조절해주는 영리한 발판과도 같았다. 덕분에 나는 지루함의 나락으로 떨어지지도, 불안의 늪에 빠지지도 않은 채, 짜릿한 몰입의 협곡 위를 계속해서 걸어 나갈 수 있었던 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 즐거움의 근원을 한 꺼풀 더 벗겨보면, 우리는 심리학의 영역을 넘어 인류의 더 깊은 기원, 즉 진화심리학의 문 앞에 서게 된다. 인류학자들은 우리의 수렵-채집인 조상들에게 생존 활동이었던 사냥과 채집이, 오늘날 우리가 생각하는 고된 &amp;lsquo;노동(toil)&amp;rsquo;과는 거리가 멀었다고 주장한다. 그것은 오히려 자율적으로 목표를 설정하고, 즉각적인 피드백 속에서 기술을 연마하며, 성공과 실패가 분명한 &amp;lsquo;놀이(play)&amp;rsquo;에 가까운 활동이었다는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나의 AI 코딩 경험은 놀라울 만큼 이 원시 시대의 &amp;lsquo;즐거운 사냥&amp;rsquo;의 구조와 닮아 있었다. 전통적인 개발 과정이 &amp;lsquo;농업&amp;rsquo;에 가깝다면 말이다. 농업은 씨앗을 심고, 오랜 시간 김을 매고 물을 주며, 수확의 기쁨을 위해 기나긴 인내의 시간을 견뎌야 한다. 그 과정에는 수많은 지루함과 예측 불가능한 변수(벌레, 가뭄)가 도사리고 있다. 반면 AI와의 협업은 &amp;lsquo;사냥&amp;rsquo;과 같았다. &amp;lsquo;새로운 기능&amp;rsquo;이라는 명확한 사냥감이 있었고, 나는 AI라는 강력한 창을 손에 쥐었다. 프롬프트를 던지고 결과를 기다리는 과정은 숨죽여 사냥감을 쫓는 추격전의 긴장감을 닮았고, 마침내 원하는 코드를 얻어냈을 때의 환희는 사냥에 성공한 뒤의 원초적인 쾌감과 다르지 않았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;물론 이 비유에는 겸손한 각주가 필요하다. 나는 광활한 평야에서 매머드를 쓰러뜨리는 위대한 사냥꾼이 아니었다. 기껏해야 마법 같은 도구를 손에 쥔 어설픈 견습생이, 뒷산에서 토끼 몇 마리를 잡고 의기양양해하는 수준에 불과했다. 중요한 것은 결과물의 크기가 아니라, 그 과정의 &amp;lsquo;형태&amp;rsquo;였다. AI는 내게서 코딩의 가장 농업적인 부분, 즉 끝없는 반복과 기약 없는 기다림이라는 &amp;lsquo;노동&amp;rsquo;의 요소를 앗아갔다. 그리고 그 자리에 창의적인 문제 해결의 핵심, 즉 새로운 해결책을 &amp;lsquo;사냥&amp;rsquo;하는 과정의 짜릿함과 즉각적인 성취감이라는 &amp;lsquo;놀이&amp;rsquo;의 감각을 채워 넣었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이는 인간과 기술, 그리고 창조의 관계에 대한 근본적이고도 낙관적인 가능성을 시사한다. 칙센트미하이는 수동적으로 받아들이는 쾌락인 &amp;lsquo;즐거움(pleasure)&amp;rsquo;과, 능동적으로 기술을 연마하고 도전을 극복하며 얻는 &amp;lsquo;재미(enjoyment)&amp;rsquo;를 명확히 구분했다. 넷플릭스를 보며 소파에 누워있는 것은 즐거움이지만, 우리를 성장시키지는 않는다. 반면 서툰 솜씨로 기타 코드를 익히고 마침내 노래 한 곡을 연주해냈을 때의 희열은, 우리를 성장시키고 삶을 풍요롭게 하는 재미에 해당한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;어쩌면 AI의 진정한 잠재력은, 우리를 모든 노동에서 해방시켜 끝없는 &amp;lsquo;즐거움&amp;rsquo;의 바다로 밀어 넣는 것이 아닐지도 모른다. 오히려 그 반대일 수 있다. AI는 노동의 가장 고되고 반복적인 부분을 자동화함으로써, &amp;lsquo;일&amp;rsquo; 자체를 성취감과 성장이 함께하는 &amp;lsquo;재미&amp;rsquo;의 영역으로 되돌려 놓을 수 있다. 그렇다면 AI가 가져올 가장 심오한 변화는 생산성 향상이라는 경제적 차원이 아니라, 노동의 본질을 재창조하는 존재론적 차원에 있을 것이다. AI는 우리를 노동에서 해방시키는 도구가 아니라, 노동 안에서 즐거움을 해방시키는 도구가 될 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;결론: 인쇄공의 견습생&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;열흘간의 폭풍 같은 여정이 끝났다. 처음에는 고작 데이트 비용 몇 푼을 아껴보려는, 지극히 개인적이고도 다소 민망한 동기에서 시작된 일이었다. 하지만 그 끝에서 나는 더 이상 AI를 단순한 연장이나 값싼 노동력으로 볼 수 없게 되었다. 그것은 내 작업 방식, 사고 과정, 나아가 창조라는 행위의 본질 자체를 뒤흔드는 거대한 충격이었다. 이 변화의 규모를 제대로 이해하기 위해, 우리는 잠시 렌즈를 뒤로 당겨 인류 역사의 가장 거대한 유추 중 하나에 기댈 필요가 있다. 지금 우리가 겪고 있는 이 순간은, 구텐베르크의 인쇄기가 가져온 정보 혁명과 가장 닮아있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 거대한 역사적 비유는 우리를 에세이의 처음으로 되돌린다. 우리가 구텐베르크의 순간을 살고 있다면, 개발자의 새로운 역할은 무엇이 될까? 답은 명확하다. 우리는 더 이상 한 글자 한 글자 정성껏 필사본을 옮겨 적던 중세의 서기(scribe)가 아니다. 인쇄술의 등장은 서기의 아름다운 손글씨 기술을 하룻밤 사이에 구식으로 만들었지만, 지식 노동 자체를 없애지는 않았다. 오히려 그 기술은 이전에는 상상조차 할 수 없었던 새로운 직업 생태계&amp;mdash;인쇄공, 식자공, 교정자, 그리고 이 모든 것을 총괄하는 출판업자&amp;mdash;를 탄생시켰다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나의 역할 역시 이와 같이 변하고 있었다. 하루 종일 모니터 앞에 앉아 모든 논리를 코드로 수동 변환하는 고된 노동은, 르네상스 시대의 인쇄공 겸 출판업자(printer-publisher)가 수행했던 더 고차원적이고 전략적인 과업으로 그 무게 중심을 옮겨가고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;인쇄술이 막 등장했던 15세기 중반의 인쇄소를 상상해보자. 그곳의 주인은 더 이상 최고의 필경사일 필요가 없었다. 그의 진짜 능력은 어떤 원고를 찍어낼지 선택하는 안목, 가장 효율적인 활자 조합을 찾아내는 기획력, 완성된 책의 오류를 잡아내는 비판적 시각, 그리고 이 모든 과정의 비용과 시간을 관리하는 경영 능력에 있었다. 그의 손은 잉크와 기름으로 더러워졌을지언정, 그의 머리는 시스템 전체를 조망하고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;바로 이 지점에서 AI 시대 개발자의 새로운 역할이 선명해진다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;첫째, 우리는 &amp;lsquo;비평가&amp;rsquo;이자 &amp;lsquo;큐레이터&amp;rsquo;가 되어야 한다. 이전의 개발자가 주어진 논리를 코드로 구현하는 &amp;lsquo;작가&amp;rsquo;에 가까웠다면, 이제 우리는 AI라는 다작(多作) 작가가 쏟아내는 수많은 초고 중에서 옥석을 가려내는 &amp;lsquo;편집자&amp;rsquo;가 되어야 한다. AI가 내놓은 코드는 정답이 아니라, 검토해야 할 제안에 불과하다. &quot;이 코드는 기술적으로 작동하는가?&quot;라는 질문을 넘어, &quot;이것이 과연 최선인가? 더 우아하고 효율적인 길은 없는가? 이 코드에 숨겨진 잠재적 편견이나 보안 위협은 없는가?&quot;라고 집요하게 되묻는 비판적 태도가 가장 중요한 자질이 된다. 나의 &amp;lsquo;퍼스널 컬러 진단기&amp;rsquo; 프로젝트가 아슬아슬하게 보여주었듯, 무비판적인 수용은 편리함을 넘어 무책임의 영역으로 우리를 이끈다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;둘째, 우리는 시스템 전체를 설계하는 &amp;lsquo;건축가&amp;rsquo;가 되어야 한다. 개별 코드를 한 줄씩 쌓아 올리는 벽돌공의 역할은 상당 부분 AI에게 위임되었다. 이제 우리의 핵심 과업은 어떤 AI 모델(GPT, Claude, Llama 등)을 선택하고, 어떤 데이터를 공급하며, 이들을 어떻게 유기적으로 연결하여 하나의 견고한 시스템을 구축할 것인지를 결정하는 것이다. 이는 마치 다양한 분야의 전문가들을 모아 하나의 거대한 건축물을 완성하는 총감독의 역할과 같다. 우리는 더 이상 연주자가 아니라, 오케스트라 전체의 조화를 책임지는 지휘자가 되어야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로, 그리고 가장 중요하게, 우리는 AI가 낳은 &amp;lsquo;고아 코드(Orphan Code)&amp;rsquo;의 법적, 윤리적 후견인이 되어야 한다. 서두에서 언급했듯, 소크라테스는 글이 &quot;언제나 아버지의 도움이 필요한 고아&quot;와 같다고 우려했다. 21세기의 AI가 생성한 코드는 그 우려의 완벽한 현현(顯現)이다. 그것은 맥락도, 의도도, 책임감도 없이 그저 존재할 뿐이다. AI는 자신이 만든 코드가 저작권을 침해하는지, 특정 집단에 대한 차별을 야기하는지 알지 못하며 신경 쓰지도 않는다. 그 모든 책임의 무게는 AI의 프롬프트에 &amp;lsquo;엔터&amp;rsquo; 키를 누른 바로 그 사람, 즉 개발자에게 고스란히 지워진다. 우리는 AI가 뱉어낸 결과물을 세상에 내놓는 배달부가 아니라, 그 결과물이 세상에 미칠 모든 영향에 대해 전적인 책임을 지는 &amp;lsquo;출판 발행인&amp;rsquo;이 되어야만 하는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kB8en/btsPA1bj1em/tsACYsrPblmHrCQuLMgf31/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kB8en/btsPA1bj1em/tsACYsrPblmHrCQuLMgf31/img.jpg&quot; data-alt=&quot;우리는 구텐베르크의 순간을 살고 있다. '필경사'가 아니라 '출판 발행인'으로서의 역량이 더 필요해진다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kB8en/btsPA1bj1em/tsACYsrPblmHrCQuLMgf31/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkB8en%2FbtsPA1bj1em%2FtsACYsrPblmHrCQuLMgf31%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;1280&quot; height=&quot;853&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;우리는 구텐베르크의 순간을 살고 있다. '필경사'가 아니라 '출판 발행인'으로서의 역량이 더 필요해진다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;서두에서 던졌던 소크라테스의 질문으로 돌아가 글을 맺는다. 생산성의 외양을 얻는 대가로 진정한 이해를 내주는 &amp;lsquo;소크라테스적 거래&amp;rsquo;의 함정을 어떻게 피할 수 있을까? 그 해답은 이 새롭고 훨씬 더 까다로운 &amp;lsquo;출판업자&amp;rsquo;의 역할을 의식적으로, 그리고 기꺼이 받아들이는 데 있다. 소크라테스가 진정으로 두려워했던 것은 지식의 외양 그 자체가 아니라, 그 외양을 진실로 착각하는 검토되지 않은 삶이었다. 그리고 출판업자의 역할이란 본질적으로 검토와 책임의 역할이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;AI에 의해 대체될 것이라는 두려움은 어쩌면 초점이 맞지 않은 것일 수 있다. 진정한 도전이자 기회는 &amp;lsquo;진화&amp;rsquo;하는 것이다. 나의 작고 보잘것없는 사이드 프로젝트 여정은 이 진화의 가능성을 보여주는 미약하지만 진실된 증거다. 이 여정의 끝에서 나는 내가 무슨 위대한 사냥꾼이나 노련한 인쇄공이 아님을 안다. 나는 그저 강력한 새 도구를 손에 쥐고 어쩔 줄 몰라 하며, 그 사용법과 책임의 무게를 이제 막 배워나가는 서툰 &amp;lsquo;견습생&amp;rsquo;에 불과하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;AI는 내 정신의 대체물이 아니다. 그것은 나로 하여금 더 나은 사상가, 더 책임감 있는 창조자, 그리고 더 사려 깊은 인간이 되도록 강요하는 강력하고, 기묘하며, 때로는 지독하게 까다로운 새로운 파트너다. 결국, AI는 쉬운 답을 주는 도구가 아니라, 훨씬 더 나은 질문을 던지게 하는 도구다. 그리고 아마도 그것이, 지혜로 나아가는 유일한 길일 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 제가 실제로 열흘 남짓한 기간동안 AI 와 협업을 통해 제작한 프로젝트들의 결고물을 공유하고자 합니다. 백수의 주머니 사정상 아쉽게도 서버가 필요한 서비스들은 일찌감치 접어야 했습니다. 덕분에 별도의 서버 없이 운영 가능한 프로젝트들만 남게 되었네요. &lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;아래 소개할 웹페이지들은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;AI 와 협을 통해 만든 프로젝트들 중에 별도의 서버 없이 동작 가능한 정적 웹사이트들 입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;&lt;span&gt;물론&amp;nbsp;기획,&amp;nbsp;디자인,&amp;nbsp;프론트엔드&amp;nbsp;전문가분들의&amp;nbsp;날카로운&amp;nbsp;눈에는&amp;nbsp;허술한&amp;nbsp;점투성이일&amp;nbsp;겁니다.&amp;nbsp;평생&amp;nbsp;땅속(서버)만&amp;nbsp;파던&amp;nbsp;개발자가&amp;nbsp;처음으로&amp;nbsp;지상에&amp;nbsp;어설프게&amp;nbsp;지어&amp;nbsp;올린&amp;nbsp;집들이니,&amp;nbsp;부디&amp;nbsp;너그러운&amp;nbsp;마음으로&amp;nbsp;구경해주시면&amp;nbsp;감사하겠습니다.&lt;br /&gt;&lt;br /&gt;여기까지, 한 백수의 기묘한 채용기에 관심 가지고 함께해주신 모든 분께 깊은 감사를 전합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://freeculture.xyz/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://freeculture.xyz/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753681955310&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;무료한 문화생활 &amp;ndash; 무료 전시&amp;middot;공연&amp;middot;축제 일정&quot; data-og-description=&quot;지역&amp;middot;카테고리&amp;middot;날짜별로 손쉽게 찾는 문화 이벤트 큐레이션 플랫폼&quot; data-og-host=&quot;freeculture.xyz&quot; data-og-source-url=&quot;https://freeculture.xyz/&quot; data-og-url=&quot;https://freeculture.xyz/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://freeculture.xyz/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://freeculture.xyz/&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;무료한 문화생활 &amp;ndash; 무료 전시&amp;middot;공연&amp;middot;축제 일정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;지역&amp;middot;카테고리&amp;middot;날짜별로 손쉽게 찾는 문화 이벤트 큐레이션 플랫폼&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;freeculture.xyz&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://alyomi.pages.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://alyomi.pages.dev/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753681964361&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;알요미 - 알리익스프레스 귀여운 직구 아이템 모음&quot; data-og-description=&quot;세상 모든 귀여움을 알요미에서! 최저가&amp;middot;할인&amp;middot;쿠폰 정보까지 한곳에.&quot; data-og-host=&quot;alyomi.pages.dev&quot; data-og-source-url=&quot;https://alyomi.pages.dev/&quot; data-og-url=&quot;https://alyomi.pages.dev&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ASpvH/hyZnaJRZwA/1niKWziKJEcaeENgIK3C7K/img.png?width=1502&amp;amp;height=918&amp;amp;face=0_0_1502_918,https://scrap.kakaocdn.net/dn/cdH7ix/hyZnf5svTN/Oc4e72NBtGFZuulxhoI7QK/img.png?width=1502&amp;amp;height=918&amp;amp;face=0_0_1502_918,https://scrap.kakaocdn.net/dn/ccroIX/hyZrwEjWsd/PYsHMV3KnkRvXSPrFSjg71/img.png?width=1362&amp;amp;height=589&amp;amp;face=0_0_1362_589&quot;&gt;&lt;a href=&quot;https://alyomi.pages.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://alyomi.pages.dev/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ASpvH/hyZnaJRZwA/1niKWziKJEcaeENgIK3C7K/img.png?width=1502&amp;amp;height=918&amp;amp;face=0_0_1502_918,https://scrap.kakaocdn.net/dn/cdH7ix/hyZnf5svTN/Oc4e72NBtGFZuulxhoI7QK/img.png?width=1502&amp;amp;height=918&amp;amp;face=0_0_1502_918,https://scrap.kakaocdn.net/dn/ccroIX/hyZrwEjWsd/PYsHMV3KnkRvXSPrFSjg71/img.png?width=1362&amp;amp;height=589&amp;amp;face=0_0_1362_589');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;알요미 - 알리익스프레스 귀여운 직구 아이템 모음&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;세상 모든 귀여움을 알요미에서! 최저가&amp;middot;할인&amp;middot;쿠폰 정보까지 한곳에.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;alyomi.pages.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://smalltalkmaker.pages.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://smalltalkmaker.pages.dev/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753681978039&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;대화 주제 생성기 - 어색한 침묵을 깨는 스몰토크 주제들&quot; data-og-description=&quot;친구, 연인, 동료와 할 말이 없을 때? 어색한 순간을 즐겁게 바꿔줄 대화 주제를 즉시 생성해보세요!&quot; data-og-host=&quot;smalltalkmaker.pages.dev&quot; data-og-source-url=&quot;https://smalltalkmaker.pages.dev/&quot; data-og-url=&quot;https://smalltalkmaker.pages.dev/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gvWtO/hyZnqeHEA2/qwuvWTKLfIB12O1YXdkEEk/img.png?width=1092&amp;amp;height=1394&amp;amp;face=0_0_1092_1394&quot;&gt;&lt;a href=&quot;https://smalltalkmaker.pages.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://smalltalkmaker.pages.dev/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gvWtO/hyZnqeHEA2/qwuvWTKLfIB12O1YXdkEEk/img.png?width=1092&amp;amp;height=1394&amp;amp;face=0_0_1092_1394');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;대화 주제 생성기 - 어색한 침묵을 깨는 스몰토크 주제들&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;친구, 연인, 동료와 할 말이 없을 때? 어색한 순간을 즐겁게 바꿔줄 대화 주제를 즉시 생성해보세요!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;smalltalkmaker.pages.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://aiface.pages.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aiface.pages.dev/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753681989881&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;AI 얼굴 나이 테스트 | aiface&quot; data-og-description=&quot;AI가 3초 만에 당신의 얼굴 나이를 분석해 드립니다!&quot; data-og-host=&quot;aiface.pages.dev&quot; data-og-source-url=&quot;https://aiface.pages.dev/&quot; data-og-url=&quot;https://aiface.pages.dev&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4lnAa/hyZnp7U3Ex/ZXSFcaV58ljE6fGlflOkx0/img.png?width=1840&amp;amp;height=1488&amp;amp;face=529_497_1638_833&quot;&gt;&lt;a href=&quot;https://aiface.pages.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://aiface.pages.dev/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4lnAa/hyZnp7U3Ex/ZXSFcaV58ljE6fGlflOkx0/img.png?width=1840&amp;amp;height=1488&amp;amp;face=529_497_1638_833');&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;AI 얼굴 나이 테스트 | aiface&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AI가 3초 만에 당신의 얼굴 나이를 분석해 드립니다!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;aiface.pages.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://aipersonalcolor.pages.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aipersonalcolor.pages.dev/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753682006854&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;AI 퍼스널 컬러 진단기 | 내 인생 컬러 찾기&quot; data-og-description=&quot;사진 한 장이면 OK! AI가 3초 만에 찾아주는 나의 진짜 퍼스널 컬러를 확인해보세요.&quot; data-og-host=&quot;aipersonalcolor.pages.dev&quot; data-og-source-url=&quot;https://aipersonalcolor.pages.dev/&quot; data-og-url=&quot;https://aipersonalcolor.pages.dev/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/53XN4/hyZqZGFHVk/tK44nAbnrvcWkNMLhPETy0/img.png?width=2090&amp;amp;height=1164&amp;amp;face=0_0_2090_1164,https://scrap.kakaocdn.net/dn/bsvjhH/hyZqPcZGUN/VJZL9gik0Zr3dnYIdXA5zK/img.png?width=2090&amp;amp;height=1164&amp;amp;face=0_0_2090_1164&quot;&gt;&lt;a href=&quot;https://aipersonalcolor.pages.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://aipersonalcolor.pages.dev/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/53XN4/hyZqZGFHVk/tK44nAbnrvcWkNMLhPETy0/img.png?width=2090&amp;amp;height=1164&amp;amp;face=0_0_2090_1164,https://scrap.kakaocdn.net/dn/bsvjhH/hyZqPcZGUN/VJZL9gik0Zr3dnYIdXA5zK/img.png?width=2090&amp;amp;height=1164&amp;amp;face=0_0_2090_1164');&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;AI 퍼스널 컬러 진단기 | 내 인생 컬러 찾기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;사진 한 장이면 OK! AI가 3초 만에 찾아주는 나의 진짜 퍼스널 컬러를 확인해보세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;aipersonalcolor.pages.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>에세이</category>
      <author>북항</author>
      <guid isPermaLink="true">https://webfirewood.tistory.com/160</guid>
      <comments>https://webfirewood.tistory.com/160#entry160comment</comments>
      <pubDate>Mon, 28 Jul 2025 15:00:55 +0900</pubDate>
    </item>
    <item>
      <title>서버실의 유령</title>
      <link>https://webfirewood.tistory.com/158</link>
      <description>&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;서론: 서버실의 유령&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;소프트웨어 시스템의 광활한 우주에는 명백한 버그와는 다른 종류의 존재들이 떠다닌다. 이들은 시스템을 무너뜨리는 소행성 충돌(System Crash)을 일으키지도, 시끄러운 경고 로그(Warning Log)라는 비명을 지르지도 않는다. 그저 항성 간의 어두운 빈 공간처럼, 조용히 시스템의 에너지를 빨아들이고 효율이라는 행성의 궤도를 미세하게 뒤트는 그림자 같은 존재들이다. 나는 이런 문제들을 '유령'이라 부르곤 한다. 거대한 신규 프로젝트라는 우주 탐사를 마치고, 안정적인 시스템 운영과 신입 교육이라는 비교적 평온한 행성 기지에서의 일상에 접어들었을 때, 우리 시스템의 보이지 않는 공간에도 그런 유령 하나가 출몰하기 시작했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;매일 아침, 어김없이 특정 시간에 시작되는 배치(Batch) 프로세스가 있었다. 이 작업의 임무는 명확했다. 하루 평균 1,000여 건에 달하는 고객 계약의 연장 데이터를 처리하는 것. 하지만 그 수행 방식은 불가사의에 가까웠다. 작업은 실패하지 않았고, 데이터가 누락되는 일도 없었다. 다만, 상식적으로 수 분 내에 끝났어야 할 이 작업이 설명할 수 없는 이유로 매번 40분에서 45분이라는 긴 시간 동안 계속되었다. 마치 매일 아침 어김없이 서버실에 나타나 시스템의 시간을 45분간 훔쳐 간 뒤, 아무 일도 없었다는 듯 홀연히 사라지는 디지털 유령과도 같았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 현상은 누구도 공식적으로 문제 삼지 않았다. 시스템은 정해진 시간 안에 작업을 마쳤고, 비즈니스는 문제없이 흘러갔다. 경영진의 보고서에는 '정상'이라는 두 글자만 찍힐 뿐이었다. 하지만 엔지니어의 세계에서 '돌아가는 것'과 '올바른 것'은 완전히 다른 차원의 이야기다. 이 45분이라는 시간은, 마치 잘 닦인 고급 자동차의 엔진에서 들려오는 미세하지만 규칙적인 소음처럼, 내 엔지니어로서의 감각을 끊임없이 자극했다. 그 소음은 당장 차를 멈춰 세울 정도는 아니었지만, 분명 어딘가 잘못되었다는 불길한 신호이자, 풀리지 않는 찝찝함으로 마음 한구석에 남아 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;639&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/opipJ/btsPClfIXaN/y5zIXmrptkVI4FRiBSwCGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/opipJ/btsPClfIXaN/y5zIXmrptkVI4FRiBSwCGk/img.png&quot; data-alt=&quot;서버실에 유령이 산다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/opipJ/btsPClfIXaN/y5zIXmrptkVI4FRiBSwCGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FopipJ%2FbtsPClfIXaN%2Fy5zIXmrptkVI4FRiBSwCGk%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;1280&quot; height=&quot;639&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;639&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서버실에 유령이 산다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 이야기는 단순히 하나의 느린 배치를 수정한 기술적 성공담이 아니다. 복잡한 문제라는 이름의 괴물을 마주했을 때, 우리는 종종 가장 최신의 번쩍이는 무기, 즉 '은 탄환(Silver Bullet)'을 먼저 찾으려 하는 경향이 있다. 하지만 이 경험은 때로는 가장 강력한 해법이 화려한 신기술이나 복잡한 프레임워크가 아닌, 문제의 본질을 꿰뚫는 단순하고 근본적인 원리의 재발견에 있음을 보여준다. 마치 미궁을 탈출하는 열쇠가 복잡한 지도가 아니라, '항상 오른손을 벽에 대고 걷는다'는 단순한 규칙이었던 것처럼 말이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 처음에는 현대적인 병렬 처리라는 강력한 기관총으로 유령을 제압하려 했던 1차 전투와, 그 후에도 사라지지 않는 미심쩍음이라는 유령의 잔상을 파고들어, 데이터베이스의 가장 기본적인 원리라는 날카로운 검으로 유령을 완전히 퇴치하게 된 2차 탐사의 여정을 담고 있다. 이 두 번의 전투는 '쉬운(easy)' 해결책과 '단순한(simple)' 해결책 사이의 깊은 철학적 차이를 탐험하는 과정이기도 했다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;제1장: 속도 저하의 해부학&lt;/h2&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;괴물의 본질: 레거시 시스템의 기이한 유산&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;모든 유령이 그렇듯, 우리 시스템의 유령 또한 과거의 역사, 즉 레거시 시스템이라는 오래된 저택의 지하실에서 태어났다. 문제의 배치 프로세스가 동작하는 이 시스템은, 마치 고대의 유물을 발굴하듯 조심스럽게 들여다봐야 하는 일종의 디지털 고고학 현장이었다. 그곳에서 발견된 계약 정보 처리 방식은, 현재 시점에서는 기이하다고밖에 표현할 수 없었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;통상적인 시스템이라면 계약을 연장할 때, 기존 계약 정보라는 문서에 새로운 종료일을 적고 도장을 찍는, 즉 &lt;code&gt;UPDATE&lt;/code&gt; 방식을 택했을 것이다. 하지만 이 시스템은 그 문서를 수정하는 대신, 완전히 &lt;b&gt;새로운 계약 문서를 생성&lt;/b&gt;하고 이전 문서는 '역사' 폴더에 보관하는 방식을 채택했다. 그리고 바로 이 지점에서, 단순한 기이함은 거대한 복잡성의 연쇄 반응으로 폭발하기 시작한다. 새로운 계약서가 발행되면, 이전 계약서에 붙어 있던 수많은 참조 딱지들&amp;mdash;무려 20개가 넘는 테이블에 흩어져 있던 기존 계약번호(Primary Key)&amp;mdash;를 일일이 떼어내어 새로운 계약서에 다시 붙여야만 했다. 즉, 단 하나의 계약 연장이 데이터베이스 전반에 걸쳐 수십 개의 &lt;code&gt;UPDATE&lt;/code&gt; 구문을 동시다발적으로 일으키는, 거대한 업데이트 폭풍을 만들어낸 것이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;과거의 어떤 비즈니스적 요구사항이나 기술적 제약이 이런 독특한 설계를 낳았는지는 이제 아무도 알 수 없었다. 여러 개발자의 손을 거치며 진화와 퇴화를 거듭해 온 시스템의 역사 속에서, 그 결정의 맥락은 이미 시간의 안갯속으로 사라진 뒤였다. 어쩌면 당시에는 데이터를 덮어쓰지 않고 모든 변경 이력을 남기는 것이 최선이라 믿었던, 신중하지만 결과적으로는 비극이 된 선택이었을지도 모른다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;분명한 것은 그 결과였다. 이 설계는 명백한 기술 부채(Technical Debt)의 한 형태였고, 우리가 매일 아침 45분씩 지불하던 처리 시간은, 바로 그 부채에 대해 시스템이 꼬박꼬박 물고 있는 값비싼 '이자'였던 셈이다. 이 구조적 문제는 간단한 비즈니스 로직 하나가 시스템 전체에 엄청난 파급 효과를 일으키는, 마치 나비의 날갯짓이 지구 반대편에서 태풍을 일으키는 것과 같은 상황을 연출하고 있었다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;최초의 가설: 길 잃은 데이터베이스&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 비정상적인 느림의 원인에 대한 나의 첫 번째 가설은 지극히 합리적이고 교과서적인 것이었다. 바로 '인덱스의 부재'였다. 데이터베이스 세계에서 인덱스(Index)는, 방대한 도서관에서 원하는 책을 찾기 위한 필수 도구인 '도서목록 카드'와 같다. 특정 책(데이터)을 찾기 위해 수십만 권의 장서를 일일이 확인하는 대신, 잘 정리된 목록 카드에서 책의 위치(데이터의 물리적 주소)를 단번에 찾아내는 방식이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나는 20개가 넘는 테이블을 업데이트하는 과정에서, 데이터베이스가 계약번호라는 단서를 가지고도 매번 도서관 전체를 헤매고 있을 것이라 추정했다. 즉, 데이터베이스가 테이블의 모든 행을 처음부터 끝까지 샅샅이 뒤지는 &lt;b&gt;풀 테이블 스캔(Full Table Scan)&lt;/b&gt;을 수행하고 있다는 가설이었다. 이는 마치 특정 계약 정보를 찾기 위해, 수억 건의 데이터가 빼곡히 들어찬 수십 권의 백과사전을 매번 첫 장부터 마지막 장까지 훑어보는 것과 같은 무모한 작업이었다. 이 가설이 맞다면, 매일 아침 데이터베이스는 수십억, 수백억 건의 데이터를 불필요하게 읽어 들이는 막대한 노동을 하고 있는 셈이고, 이것이 바로 성능 저하의 핵심일 것이라 짐작했다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;기술 심층 탐구: 풀 테이블 스캔의 보이지 않는 노동&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;풀 테이블 스캔(Full Table Scan, FTS)은 데이터베이스가 특정 데이터를 찾기 위해 사용하는 가장 원시적이면서도 정직한, 그리고 때로는 처절한 방식이다. 테이블의 첫 번째 데이터 블록부터 마지막 블록까지, 마치 컨베이어 벨트 위의 상자들을 하나씩 모두 열어보는 것처럼 순차적으로 검사한다. 이 작업의 복잡도는 O(n)으로, 테이블의 행 수(n)에 정비례하여 처리 시간이 끝없이 늘어난다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;성능 저하의 주범은 단연 입출력(I/O)이다. CPU가 눈 깜짝할 사이에 수 많은 연산을 처리하는 동안, 하드디스크는 겨우겨우 얼마 안되는 데이터를 읽어올 뿐이다. 이 둘 사이의 속도 차이는 슈퍼카와 달팽이의 경주에 비유할 수 있다. FTS는 이 느린 달팽이에게 운동장 전체를 기어가게 만드는 것과 같아서, 방대한 양의 디스크 I/O를 유발하며 시스템에 큰 부담을 준다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;물론 데이터베이스의 두뇌라 할 수 있는 &lt;b&gt;쿼리 옵티마이저(Query Optimizer)&lt;/b&gt;가 어리석어서 FTS를 선택하는 것은 아니다. 옵티마이저는 통계 정보를 기반으로 여러 실행 계획의 비용을 치밀하게 계산하고 가장 효율적인 방법을 선택하는, 냉철한 회계사와 같다. 다음과 같은 경우에는 FTS가 인덱스를 사용하는 것보다 합리적인 선택이 될 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;조회 대상 데이터가 너무 많을 경우:&lt;/b&gt; 테이블의 상당 부분(일반적으로 5~10% 이상)을 읽어야 할 때가 그렇다. 이 경우, 인덱스를 통해 데이터의 위치를 일일이 찾아다니는 수많은 &lt;b&gt;무작위 I/O(Random I/O)&lt;/b&gt;를 발생시키는 것보다, 차라리 처음부터 끝까지 한 번에 쭉 읽는 &lt;b&gt;순차적 I/O(Sequential I/O)&lt;/b&gt;가 더 빠를 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 가능한 인덱스가 없는 경우:&lt;/b&gt; &lt;code&gt;WHERE&lt;/code&gt; 절의 조건에 맞는 '도서목록 카드'가 아예 존재하지 않을 때, 옵티마이저에게는 FTS 외에 다른 선택지가 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우리의 경우는 명백히 후자에 가까워 보였다. 매번 단 하나의 계약번호에 해당하는 데이터를 찾기 위해 테이블 전체를 읽는 것은, 도서관에서 책 한 권을 빌리기 위해 모든 서가를 뒤지는 것과 같은 비극적인 비효율이었다. 이 가설이 맞다면, 해결책은 이론적으로는 간단했다. 20여 개 테이블의 계약번호 컬럼에 인덱스라는 '도서목록 카드'를 만들어주는 것이었다. 이 카드는 데이터베이스의 가장 근본적인 자료구조인 &lt;b&gt;B-Tree&lt;/b&gt; 형태로 구현되어, 데이터가 아무리 많아져도 O(log n)이라는 경이로운 속도로 원하는 데이터를 찾아갈 수 있게 해주는 마법 같은 존재다. 이 마법만 부릴 수 있다면, 유령은 쉽게 잡힐 것 같았다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;제2장: 병렬 처리와 파다완의 길&lt;/h2&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;새로운 희망: 파다완에게 주어진 첫 번째 임무&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;모든 오래된 성에 유령이 출몰하듯, 모든 복잡한 시스템에는 후임자들을 위한 기묘한 미스터리가 숨어 있기 마련이다. 그리고 이 45분짜리 유령은 이제 막 팀에 합류하여 뜨거운 열정과 재능으로 빛나던 신입 개발자에게 더할 나위 없이 좋은 첫 번째 임무처럼 보였다. 나는 이 문제를 그에게 온전히 맡겨보기로 했다. 이는 단순히 일손을 더는 차원의 결정이 아니었다. 명확한 문제 상황을 제시하고, 그가 스스로 원인을 분석하고 해결책을 찾아 나가는 이상적인 '문제 해결의 여정'을 경험하게 해주고픈 선배로서의 욕심이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;훌륭한 멘토링은 정답을 알려주는 것이 아니라, 멘티가 스스로 답을 향한 지도를 그려나가는 과정을 묵묵히 지켜봐 주는 것이라 믿는다. 제다이 마스터가 파다완에게 광선검 만드는 법을 직접 가르치는 대신, 필요한 부품이 숨겨진 동굴의 위치만을 알려주듯이 말이다. 나는 그가 스스로 가설을 세우고, 해결책을 설계하며, 그 과정에서 부딪히는 기술적, 조직적 난관을 헤쳐나가는 경험 그 자체를 통해 한 뼘 더 성장하기를 바랐다. 그리고 그는 기대 이상으로 빠르게 문제의 핵심에 접근했다. 시스템의 동작을 며칠간 면밀히 분석한 끝에, 나 역시 어렴풋이 짐작했던 '인덱스 부재'가 성능 저하의 핵심 원인일 것이라는 결론에 독자적으로 도달했다. 그의 눈빛은 자신의 분석에 대한 확신으로 빛나고 있었다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;현자의 지혜: 예상치 못한 장애물과 보이지 않는 비용&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;자신만의 분석으로 얻은 결론에 자신감을 얻은 그는, 마치 전설의 검을 뽑아 들 듯 당당하게 데이터베이스 관리자(DBA)에게 계약번호 컬럼에 대한 인덱스 생성을 요청했다. 그러나 돌아온 답변은 이 이야기의 첫 번째 극적인 반전이었다. &quot;그 요청은 승인할 수 없습니다. 사실, 과거에 바로 그 인덱스가 있었지만 저희가 직접 제거했습니다.&quot;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 거절은 단순한 반대가 아니었다. 그 이면에는 시스템이라는 거대한 함선의 모든 항해 기록과 설계도를 꿰고 있는, 노련한 기관장의 깊은 통찰이 담겨 있었다. DBA는 과거 해당 컬럼에 인덱스가 존재했으나, 이 시스템의 독특한 '수정' 방식, 즉 &lt;code&gt;UPDATE&lt;/code&gt;가 아닌 &lt;code&gt;INSERT&lt;/code&gt;와 연쇄 &lt;code&gt;UPDATE&lt;/code&gt;의 조합이 일으키는 과도한 쓰기 작업 때문에 오히려 시스템 전반의 성능을 저해하여 의도적으로 제거했던 이력을 차분히 설명해주었다. 이는 개발자와 DBA 사이의 오랜 문화적 간극을 보여주는 동시에, 협업의 중요성을 일깨우는 순간이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;개발자는 특정 쿼리의 '읽기(Read)' 성능이라는 망원경으로 눈앞의 행성을 관측하는 경향이 있지만, DBA는 그 행성의 움직임이 전체 항성계에 미치는 중력까지 계산하는 천문학자의 시각을 가져야 한다. 인덱스가 걸린 컬럼의 값이 변경될 때마다, 데이터베이스는 단순히 테이블의 데이터만 수정하는 것이 아니다. B-Tree 구조로 정교하게 짜인 인덱스 데이터 또한 정렬 순서에 맞게 재구성해야 하는, 보이지 않는 막대한 비용을 치른다. 이 과정에서 다음과 같은 값비싼 작업들이 수면 아래에서 벌어진다.&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;페이지 분할(Page Splits)이라는 연쇄 충돌:&lt;/b&gt; 데이터가 저장되는 페이지(블록)에 더는 새로운 데이터를 삽입할 공간이 없을 때, 데이터베이스는 페이지를 두 개로 나누고 기존 데이터의 일부를 새 페이지로 옮긴다. 이는 마치 만원 버스에 승객 한 명이 더 타려고 할 때, 버스를 두 대로 쪼개고 승객 절반을 다른 버스로 옮긴 뒤, 노선표까지 새로 고쳐 쓰는 것과 같은 대혼란이다. 이 작업은 상당한 I/O와 트랜잭션 로그를 발생시키며, 데이터의 물리적 순서를 흐트러뜨려 인덱스 단편화(fragmentation)라는 후유증을 남긴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;잠금 경합(Lock Contention)이라는 교통 체증:&lt;/b&gt; &lt;code&gt;UPDATE&lt;/code&gt; 작업 시 데이터베이스는 데이터 페이지뿐만 아니라 관련된 인덱스 페이지에도 잠금(Lock)을 건다. 인덱스가 많을수록 잠가야 할 대상이 늘어나고 잠금 유지 시간도 길어져, 다른 트랜잭션들이 하염없이 신호를 기다리는 병목 현상이 발생할 가능성이 커진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;DBA는 20개가 넘는 테이블에서 매일 수천 건의 계약번호가 변경되는 이 작업의 특성을 정확히 이해하고 있었다. 여기에 모두 인덱스를 걸 경우, 배치 작업의 읽기 성능은 개선될지 몰라도, 시스템 전반의 쓰기 성능 저하와 잠금 문제로 인해 더 큰 재앙, 즉 '쓰기 증폭(Write Amplification)' 현상을 초래할 수 있다는 현명한 판단을 내렸던 것이다. 그의 설명은, 빠른 항해를 위해 무작정 돛을 더 다는 것이 아니라, 배의 균형과 바람의 방향까지 고려해야 한다는 베테랑 항해사의 지혜와도 같았다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;파다완의 전환: 현대적인 무기를 향한 열망&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;'인덱스'라는 왕도를 사용할 수 없게 된 신입 개발자는 잠시 좌절했지만, 이내 새로운 길을 모색했다. 그의 선택은 현대 소프트웨어 공학의 가장 강력하고 매력적인 무기 중 하나인 &lt;b&gt;병렬 처리(Parallel Processing)&lt;/b&gt;였다. 그의 논리는 명쾌하고 현대적이었다. &quot;한 명의 사서가 백과사전 전체를 읽는 것이 느리다면, 여러 명의 속독가를 동시에 고용해 각기 다른 권을 읽게 하면 되지 않을까요?&quot;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boWmob/btsPBhZzE4z/Qv2cUCt3ixBgFAg3T7DnH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boWmob/btsPBhZzE4z/Qv2cUCt3ixBgFAg3T7DnH0/img.png&quot; data-alt=&quot;&amp;quot;한 명의 사서가 백과사전 전체를 읽는 것이 느리다면, 여러 명의 속독가를 동시에 고용해 각기 다른 권을 읽게 하면 되지 않을까요?&amp;quot;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boWmob/btsPBhZzE4z/Qv2cUCt3ixBgFAg3T7DnH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboWmob%2FbtsPBhZzE4z%2FQv2cUCt3ixBgFAg3T7DnH0%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;1280&quot; height=&quot;486&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&quot;한 명의 사서가 백과사전 전체를 읽는 것이 느리다면, 여러 명의 속독가를 동시에 고용해 각기 다른 권을 읽게 하면 되지 않을까요?&quot;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그는 당시 프로젝트의 기술 스택인 &lt;code&gt;Java 7&lt;/code&gt; 환경에서 사용할 수 있는 가장 진보된 동시성 처리 도구인 &lt;b&gt;&lt;code&gt;Fork/Join Framework&lt;/code&gt;&lt;/b&gt;를 해결책으로 제시했다. 솔직히 나는 이 선택에 대해 약간의 회의감이 들었다. 이 작업의 본질은 CPU 연산(CPU-Bound)이 아니라 데이터베이스 I/O 대기 시간(I/O-Bound)이었기 때문이다. 그의 해법은 마치 교통 체증으로 꽉 막힌 도로에서 더 빠른 차로 갈아타려는 시도처럼 보였다. 차의 성능이 아무리 좋아도, 도로 자체가 뚫리지 않으면 무용지물이기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 나는 멘토로서 그의 선택을 존중하고 함께 나아가기로 결정했다. 이는 &lt;b&gt;드라이퍼스 모델(Dreyfus Model of Skill Acquisition)&lt;/b&gt;에서 '고급 초심자(Advanced Beginner)' 단계에 있는 학습자가 규칙을 넘어 상황에 맞게 지식을 적용해보는 중요한 과정이기 때문이었다. 그가 직접 부딪혀보고 그 기술의 장점과 한계를 체감하는 것이, 내가 백 마디 이론을 설명해주는 것보다 훨씬 값진 경험이 되리라 믿었다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;기술 심층 탐구: 우아한 춤과 어색한 무대 - Fork/Join 프레임워크의 명과 암&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Fork/Join&lt;/code&gt; 프레임워크는 '분할 정복(divide and conquer)' 알고리즘을 병렬 처리에 우아하게 적용한 구현체이다. 그 작동 원리는 '작업 훔치기(Work-Stealing)'라는 혁신적인 알고리즘에 기반한다. 각 스레드가 자신의 작업 큐(Queue)를 가지고 처리하되, 할 일이 없어진 스레드는 다른 바쁜 스레드의 큐에서 일을 '훔쳐와' 처리함으로써 CPU 코어를 한순간도 쉬지 않고 최대한 활용하는 방식이다. 이는 마치 효율적인 레스토랑 주방에서, 자기 파트의 일이 끝난 요리사가 잠시도 쉬지 않고 가장 바쁜 동료의 일을 돕는 모습과도 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 우아한 춤에는 치명적인 약점이 있다. 바로 &lt;b&gt;I/O 집중적(I/O-Bound) 작업&lt;/b&gt;이라는 어색한 무대 위에서는 그 진가를 발휘하기 어렵다는 점이다. &lt;code&gt;Fork/Join&lt;/code&gt; 프레임워크는 이미지 렌더링이나 복잡한 수학 연산처럼 CPU가 쉴 틈 없이 일하는 &lt;b&gt;CPU 집중적(CPU-Bound) 작업&lt;/b&gt;에 최적화되어 있다. 그러나 우리의 작업은 대부분의 시간을 데이터베이스가 &lt;code&gt;UPDATE&lt;/code&gt; 쿼리를 처리하고 응답하기를 '기다리는' 데 사용한다. &lt;code&gt;join()&lt;/code&gt; 메서드는 하위 작업이 끝날 때까지 현재 스레드를 차단(block)시키는데, I/O 대기 상황에서 이는 귀중한 스레드 자원의 낭비이며, 작업 훔치기 알고리즘의 효율을 크게 떨어뜨린다. CPU는 놀고 있는데 스레드만 하염없이 기다리는, 비효율의 극치가 발생하는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이런 I/O 집중적 작업에는 &lt;code&gt;Fork/Join&lt;/code&gt; 프레임워크보다 오히려 &lt;b&gt;&lt;code&gt;ExecutorService&lt;/code&gt;와 고정된 크기의 스레드 풀(Fixed Thread Pool)&lt;/b&gt;을 사용하는 것이 더 전통적이고 적합한 접근 방식일 수 있다. 이는 작업 훔치기와 같은 복잡한 메커니즘 없이, 정해진 수의 스레드가 꾸준히 I/O 작업을 요청하고 응답을 기다리는 단순하고 예측 가능한 모델이다. 스레드가 I/O 대기로 인해 차단되더라도, 이는 예상된 동작이며 스레드 풀의 크기를 적절히 조절함으로써 전체 시스템의 처리량을 제어할 수 있다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;가시적인 승리, 그리고 남겨진 그림자&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이론적 한계에도 불구하고, 결과는 성공적이었다. 우리는 마치 오케스트라의 지휘자가 되어 각 악기(스레드)의 소리가 최적의 화음을 내도록 조율하는 것처럼, 수많은 테스트를 진행했다. 스레드 개수와 데이터베이스 커넥션 풀 크기라는 두 변수를 미세하게 조정하며 최적의 조합을 찾아 나갔고, 마침내 스레드 10개와 커넥션 풀 10개라는 '스위트 스폿(Sweet Spot)'을 발견했다. 그 결과, 45분에 달하던 처리 시간은 &lt;b&gt;10분대&lt;/b&gt;로 극적으로 단축되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 성과는 여러 면에서 의미가 있었다. 비즈니스적으로는 매일 30분 이상의 시스템 자원을 절약했고, 팀 차원에서는 신입 개발자가 성공적으로 첫 임무를 완수하여 정규직으로 전환되는 기쁨을 안겨주었다. 그의 얼굴에 번진 성취감과 자신감은 그 어떤 성능 지표보다 값진 결과였다. 이는 실용적인 문제 해결이 개인의 성장과 조직의 성공에 어떻게 기여할 수 있는지를 보여주는 훌륭한 사례였다. 유령은 완전히 사라지지 않았지만, 우리는 그 활동 시간을 크게 줄여놓는 데 성공했다. 첫 번째 전투는 분명 우리의 승리였다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;제3장: 꺼지지 않는 불씨&lt;/h2&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;유령은 아직 그곳에 있었다&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;45분에서 10분으로. 78%의 성능 개선. 누가 봐도 성공적인 프로젝트였고, 팀은 환호했다. 신입 개발자는 자신의 역량을 증명하며 팀의 정식 일원이 되었고, 그의 얼굴에 번진 성취감과 자신감은 그 어떤 성능 지표보다 값진 결과였다. 비즈니스적으로는 매일 30분 이상의 시스템 자원을 절약했으니, 우리는 유령을 성공적으로 제압한 듯 보였다. 모든 것이 제자리를 찾은 것 같았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 내 마음속에는 잘 닦인 자동차 계기판에 들어온 '엔진 체크' 경고등처럼, 작지만 선명한 찝찝함이 꺼지지 않고 있었다. &quot;10분도 여전히 너무 길다.&quot; 이 목소리는 축하 파티의 소음 속에서도 명확하게 들려왔다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;1,000여 건의 데이터를 처리하는 데 10분, 즉 600초가 걸린다는 것은 한 건의 계약을 처리하는 데 평균 0.6초가 소요된다는 의미였다. 현대 컴퓨팅의 관점에서 0.6초는 영겁과도 같은 시간이다. 빛이 지구를 일곱 바퀴 반이나 돌고, 평범한 CPU가 수십억 번의 연산을 처리할 수 있는 시간 동안, 우리의 데이터베이스는 고작 데이터 한 묶음을 처리하기 위해 끙끙대고 있었던 것이다. 이 속도는 마치 최신 스포츠카를 타고 시속 10킬로미터로 달리는 것과 같은 부조리함이었다. 차는 분명 앞으로 나아가고 있지만, 그 방식은 명백히 잘못되어 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우리의 첫 번째 해결책은 문제의 근원을 제거한 것이 아니었다. 그것은 마치 고금리 카드빚에 시달리다가, 이자 납부일에 맞춰 최소 결제 금액만 겨우 입금한 것과 같았다. 당장의 독촉 전화(시스템 경고)는 멈췄지만, 무시무시한 원금(근본적인 비효율)은 고스란히 남아 매일 조용히 자원을 갉아먹는 '이자'를 발생시키고 있었다. 유령은 쫓겨난 것이 아니라, 그저 활동 시간을 줄여 더욱 교묘하게 시스템의 그림자 속에 숨어들었을 뿐이었다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;철학의 교차점: '쉬움(Easy)'과 '단순함(Simple)'의 깊은 계곡&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 지적 불만족의 근원을 파고들다 보면, 소프트웨어 공학의 가장 심오한 철학적 질문과 마주하게 된다. 나는 클로저(Clojure) 언어의 창시자인 리치 히키(Rich Hickey)가 그의 명강의 &quot;Simple Made Easy&quot;에서 설파한 '쉬움(easy)'과 '단순함(simple)'의 구분에 깊이 공감한다.&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;쉬움(Easy)&lt;/b&gt;은 '가까이 있음(near)'을 의미한다. 즉, 우리에게 익숙하거나, 손에 닿기 가깝거나, 이해하기 편한 도구를 뜻한다. &lt;code&gt;Fork/Join&lt;/code&gt; 프레임워크를 이용한 우리의 첫 번째 해결책은 명백히 '쉬운' 길이었다. 병렬 처리는 현대 개발자에게 매우 친숙한 개념이며, &lt;code&gt;Java 7&lt;/code&gt;이라는 주어진 환경에서 가장 손쉽게 적용할 수 있는 강력한 망치였다. 우리는 눈앞의 못(느린 속도)을 보고, 가장 가까이 있는 망치를 집어 들었을 뿐이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단순함(Simple)&lt;/b&gt;은 '하나의 꼬임(one-fold)'을 의미한다. 이는 여러 요소가 복잡하게 얽혀있지 않은 상태, 즉 하나의 역할, 하나의 개념, 하나의 책임만을 갖는 본질적인 상태를 말한다. 그 반대는 여러 실이 뒤엉켜 풀 수 없게 된 '복잡함(complex)'이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우리의 근본적인 문제는 데이터 접근 방식이 처음부터 '복잡하다'는 것이었다. 하나의 계약을 갱신하기 위해 20개가 넘는 테이블 전체를 스캔해야 하는 로직 자체가 여러 책임과 맥락이 뒤얽힌 복잡성의 결정체였다. 첫 번째 해결책은 이 복잡한 구조를 그대로 둔 채, '쉬운' 병렬 처리 기술을 덧씌워 실행 속도라는 '증상'만을 완화한 것이었다. 우리는 비효율적인 작업을 더 빨리 수행하게 되었을 뿐, 작업 자체의 비효율을 제거하지는 못했다. 이는 마치 엉킨 실타래를 풀 생각은 않고, 그저 더 많은 실을 감아 겉보기에만 그럴듯하게 만든 것과 같았다.&lt;/p&gt;
&lt;table style=&quot;height: 210px;&quot; width=&quot;865&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;차원&lt;/th&gt;
&lt;th&gt;해결책 1: 병렬 처리 (쉬운 길)&lt;/th&gt;
&lt;th&gt;해결책 2: 복합 인덱싱 (단순한 길)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;핵심 해결 문제&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;증상 (실행 시간)&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;근본 원인 (데이터 접근 경로)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;기저의 복잡성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;높음 (비효율적인 FTS를 감추고 확장함)&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;낮음 (FTS 자체를 제거함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;자원 사용량&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;CPU 집약적, I/O에 대한 높은 스레드 경합&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;I/O 효율적, 최소한의 자원 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;유지보수성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;복잡한 동시성 프레임워크에 대한 이해 필요&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;표준 SQL 및 DB 지식 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;해법의 미학&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;무차별 대입 - 더 큰 망치&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;외과적 정밀함 - 더 날카로운 메스&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 표는 두 가지 접근 방식의 철학적 차이를 명확히 보여준다. 우리는 I/O 대기가 문제의 본질임에도 불구하고, CPU를 더 많이 사용하는 방식으로 문제를 풀려 했다. 도서관 전체를 뒤져야 하는 비효율적인 업무 프로세스는 그대로 둔 채, 사서들에게 더 빠른 운동화를 신겨준 셈이다. 진정으로 우아한 해결책은 복잡함을 더 잘 관리하는 것이 아니라, 복잡함 자체를 제거하는 데서 나온다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;장인정신의 발현: '왜?'라는 질문의 무게&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 깨달음은 누구의 지시도, 어떤 비즈니스 요구사항도 아니었다. 그것은 더 나은 해결책이 존재할 것이라는 엔지니어로서의 직감이자, '충분히 좋음'에 안주하기를 거부하는 장인정신의 발현이었다. 드라이퍼스 기술 습득 모델에 빗대자면, 주어진 문제를 해결하는 '중급자(Competent)'의 단계를 넘어, 문제의 본질과 더 넓은 맥락을 이해하는 '숙련자(Proficient)'로 나아가려는 몸부림이었을지도 모른다. 숙련자는 단순히 규칙을 따르는 것을 넘어, 그 규칙이 왜 존재하는지, 그리고 때로는 그 규칙을 깨야 하는 이유를 이해한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;여기에 후배가 시작한 개선 작업을 선배로서 더 나은 방식으로 마무리지어야 한다는 책임감도 한몫했다. 그가 훌륭하게 첫 전투를 승리로 이끌었다면, 나는 이 지루한 전쟁을 완전히 끝내야 할 의무가 있었다. 이 문제는 이제 해결해야 할 업무가 아니라, 지적 호기심을 충족시키고 기술적 완결성을 추구하기 위한 개인적인 도전 과제가 되었다. 나는 문제의 본질을 해결하기 위해, 유령을 완전히 퇴치하기 위한 마지막 구마 의식을 준비하기 위해 다시 한번 키보드 앞에 앉았다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;제4장: 최후의 구마 의식&lt;/h2&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;기본으로의 회귀: 현미경과 나침반&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;두 번째 여정은 처음과 근본적으로 달랐다. 화려한 병렬 처리 프레임워크나 최신 기술 라이브러리를 검색하는 대신, 나는 가장 기본적인 도구 두 가지만을 챙겨 들었다. 하나는 시스템의 가장 작은 세포까지 들여다볼 수 있는 '현미경', 즉 데이터베이스 스키마와 비즈니스 로직 그 자체였고, 다른 하나는 길을 잃지 않게 해 줄 '나침반', 즉 &quot;왜?&quot;라는 근본적인 질문이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나는 기존의 가설과 제약, 심지어 1차 최적화의 성공 경험까지 모두 백지상태로 되돌렸다. 마치 처음 보는 시스템을 분석하는 신입의 마음으로, 테이블 구조와 데이터의 흐름, 그리고 그 사이를 흐르는 쿼리들을 면밀히 재검토했다. 이전의 접근이 '어떻게 하면 이 느린 작업을 더 빨리 실행시킬까?'에 대한 고민이었다면, 이번의 질문은 '애초에 이 작업은 왜 느릴 수밖에 없는가?'로 바뀌었다. 문제는 데이터에 있고, 답 또한 데이터 안에 있을 것이라는 믿음, 그것이 유일한 등대였다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;유레카의 순간: 범죄 현장의 재구성&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;결정적인 돌파구는 시스템을 하나의 거대한 범죄 현장으로 보고, 용의자(&lt;code&gt;기존계약번호&lt;/code&gt;)의 행적뿐만 아니라 범행 패턴 전체를 분석하면서 찾아왔다. 계약번호가 유일한 식별자는 맞지만, 데이터를 조회할 때 사용할 수 있는 유일한 '단서'는 아니라는 사실을 깨달은 것이다. 각 테이블에는 계약번호 외에도 계약일, 멤버십 종류, 상태 코드, 계약 시작일 및 종료일 등 다양한 속성들이 마치 현장에 남겨진 지문처럼 존재했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 가장 중요한 발견은, 우리가 매일 아침 처리해야 할 1,000여 건의 데이터가 무작위로 흩어진 집합이 아니라, 명확한 시간적 패턴을 따르는 연쇄 사건이라는 점이었다. 비즈니스 로직을 형사의 집요함으로 파고들자, 수정 대상이 되는 20여 개 테이블의 데이터가 원본 계약이 생성된 '계약일'로부터 3일 이내에 함께 생성된 데이터라는 사실이 드러났다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이것은 단순한 단서를 넘어, 사건의 전모를 밝혀줄 결정적 증거였다. 우리는 더 이상 수억 건의 데이터가 쌓인 광활한 도시 전체를 이 잡듯 뒤지며 용의자를 찾을 필요가 없었다. '3일'이라는 명확한 시간과 장소로 용의자의 동선이 특정된 것이다. 탐색 범위는 도시 전체에서 작은 동네 몇 군데로 획기적으로 좁혀졌다. 이제 우리에게 필요한 것은 그 동네를 가장 효율적으로 수색할 수 있는 지도, 즉 새로운 인덱스 전략이었다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;기술 심층 탐구: 전화번호부와 파르테논 신전 - 복합 인덱스 설계의 미학&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 결정적 단서를 활용할 무기는 바로 &lt;b&gt;복합 인덱스(Composite Index)&lt;/b&gt;였다. 이는 둘 이상의 컬럼을 묶어 하나의 정렬된 지도를 만드는 기술이다. 이 기술의 정교함을 이해하기 위해, 우리는 '전화번호부'라는 익숙한 비유에서 시작해 '파르테논 신전'이라는 건축학적 비유로 나아가 볼 필요가 있다.&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;전화번호부와 좌측 접두사 규칙: B+Tree의 물리적 현실&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;복합 인덱스의 작동 원리는 '성(Last Name), 이름(First Name)' 순으로 정렬된 전화번호부와 정확히 일치한다. 이 규칙을 &lt;b&gt;좌측 접두사 규칙(Left-Prefix Rule)&lt;/b&gt;이라 부른다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;WHERE last_name = '김'&lt;/code&gt; : 매우 효율적이다. 전화번호부의 'ㄱ' 섹션으로 바로 가면 된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE last_name = '김' AND first_name = '민준'&lt;/code&gt; : 역시 효율적이다. '김'씨들을 찾은 뒤, 그 안에서 '민준'을 찾으면 된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE first_name = '민준'&lt;/code&gt; : 완전히 비효율적이다. '민준'이라는 이름은 책 전체에 흩어져 있으므로, 전화번호부 전체를 한 장씩 넘겨봐야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 규칙은 데이터베이스의 인덱스가 실제로 &lt;b&gt;B+Tree&lt;/b&gt;라는 자료구조로 구현되기 때문에 발생한다. B+Tree는 데이터를 물리적으로 정렬하여 저장하는데, &lt;code&gt;(계약일, 계약번호)&lt;/code&gt; 순서의 인덱스는 데이터를 먼저 '계약일' 순으로 차곡차곡 쌓고, 같은 계약일 안에서는 다시 '계약번호' 순으로 정렬해 놓는다. 데이터베이스가 계약일 정보 없이 계약번호만으로 데이터를 찾으려는 시도는, 첫 글자를 모른 채 사전을 찾으려는 것과 같은 막막한 일이 되어버린다. 결국 데이터베이스는 지도를 포기하고 마을 전체를 걸어 다니는 &lt;b&gt;풀 테이블 스캔(Full Table Scan)&lt;/b&gt;을 선택할 수밖에 없었던 것이다.&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;파르테논 신전과 컬럼 순서의 미학: 카디널리티와 선택도의 예술&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 복합 인덱스의 컬럼 순서는 어떻게 정해야 할까? 여기서 우리는 데이터베이스 설계자에서 고대 그리스의 건축가로 변신해야 한다. 가장 중요한 원칙은 &lt;b&gt;가장 선택도가 높은(highly selective) 컬럼을 맨 앞에 두는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;카디널리티(Cardinality):&lt;/b&gt; 컬럼이 가진 고유한 값의 개수다. 모든 값이 다른 주민등록번호는 카디널리티가 매우 높고, '남/여' 두 값만 가진 성별은 매우 낮다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선택도(Selectivity):&lt;/b&gt; 특정 값으로 조회했을 때, 얼마나 적은 수의 행이 선택되는지를 나타내는 척도. 카디널리티가 높을수록 선택도도 높다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;복합 인덱스를 설계하는 것은 파르테논 신전을 짓는 것과 같다. 가장 넓고 튼튼하며 하중을 잘 견디는 &lt;b&gt;도리아식 기둥(가장 선택도가 높은 컬럼)&lt;/b&gt;을 건물의 가장 아래, 즉 인덱스의 맨 앞에 놓아야 한다. 그 위에 좀 더 장식적인 &lt;b&gt;이오니아식 기둥(다음 선택도 컬럼)&lt;/b&gt;, 그리고 가장 위에는 화려한 &lt;b&gt;코린트식 기둥(가장 선택도가 낮은 컬럼)&lt;/b&gt;을 배치하는 순서다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;852&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sNbC9/btsPCi4j2eH/TMNjc70uy4ktsvy1AEWhv0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sNbC9/btsPCi4j2eH/TMNjc70uy4ktsvy1AEWhv0/img.jpg&quot; data-alt=&quot;복합 인덱스를 설계하는 것은 파르테논 신전을 짓는 것과 같다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sNbC9/btsPCi4j2eH/TMNjc70uy4ktsvy1AEWhv0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsNbC9%2FbtsPCi4j2eH%2FTMNjc70uy4ktsvy1AEWhv0%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;1280&quot; height=&quot;852&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;852&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;복합 인덱스를 설계하는 것은 파르테논 신전을 짓는 것과 같다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;데이터베이스의 뇌, 즉 &lt;b&gt;쿼리 옵티마이저(Query Optimizer)&lt;/b&gt;는 이 견고한 구조를 아주 좋아한다. 옵티마이저는 맨 앞의 도리아식 기둥(&lt;code&gt;계약일 BETWEEN 어제 AND 오늘&lt;/code&gt;) 조건으로 수백만 개의 데이터 행을 수백 개로 단번에 걸러낸다. 그리고 그 좁혀진 결과 집합 내에서만 다음 이오니아식 기둥(&lt;code&gt;계약번호 = '...'&lt;/code&gt;) 조건으로 다시 한번 거르는 방식으로 극도의 효율을 추구한다. 이는 비효율적인 랜덤 I/O를 최소화하고, 예측 가능한 순차 I/O로 작업을 전환하여 I/O 비용을 극적으로 낮추는, 데이터베이스 세계의 연금술과도 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 원칙에 따라 나는 20여 개의 각 테이블 특성을 분석했다. 그리고 &lt;code&gt;(계약일, 멤버십이름, 기존계약번호)&lt;/code&gt; 와 같이, 조회 범위를 가장 효과적으로 좁힐 수 있는 컬럼들의 최적의 조합과 순서를 하나하나 설계했다. 이는 단순히 인덱스를 추가하는 행위가 아니라, 각 테이블의 데이터 접근 경로를 새로 디자인하는 건축 행위에 가까웠다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;협업의 촉매: DBA와의 두 번째 대화&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;완벽하게 설계된 20여 개의 복합 인덱스 설계안과, 이 인덱스를 사용했을 때와 아닐 때의 예상 실행 계획(Execution Plan) 분석 자료를 들고 나는 다시 DBA를 찾아갔다. 이번의 대화는 첫 번째와는 질적으로 달랐다. 나는 더 이상 &quot;인덱스가 필요합니다&quot;라는 막연한 요구를 하는 개발자가 아니었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;이 테이블에는 &lt;code&gt;(계약일, 멤버십이름)&lt;/code&gt; 순서의 복합 인덱스를 제안합니다. &lt;code&gt;계약일&lt;/code&gt; 조건으로 조회 대상을 99% 이상 줄일 수 있어 선택도가 매우 높고, 이를 통해 풀 테이블 스캔을 인덱스 범위 스캔(Index Range Scan)으로 변경할 수 있습니다. 또한, &lt;code&gt;계약일&lt;/code&gt;을 인덱스 선두에 둠으로써 쓰기 작업이 인덱스의 특정 '핫스팟'에 몰리는 현상을 방지하고, 과거에 우려하셨던 쓰기 부하와 잠금 경합(Lock Contention) 문제 역시 최소화할 수 있습니다. 이로 인한 성능 향상 효과가 쓰기 작업의 유지보수 비용을 압도적으로 상회할 것으로 기대됩니다.&quot;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이는 DBA의 전문성과 그들의 우려(쓰기 부하, 유지보수)를 깊이 존중하고 이해했음을 보여주는 접근이었다. 개발자가 데이터베이스의 원리를 이해하고 DBA의 언어로 소통할 때, 그들은 더 이상 요청자와 승인자라는 수직적 관계가 아닌, 시스템의 성능을 함께 고민하는 수평적 파트너가 될 수 있다. 사일로(silo)가 허물어지고 진정한 데브옵스(DevOps) 문화가 싹트는 순간이었다. DBA는 제안을 흔쾌히 받아들였고, 우리는 함께 인덱스 생성 작업을 진행했다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;최종 결과: 유령의 소멸&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;새로운 복합 인덱스가 적용되고, 배치 애플리케이션의 쿼리가 이 인덱스를 사용하도록 수정한 뒤, 결과는 놀라웠다. 10분 넘게 걸리던 작업이 &lt;b&gt;1분 미만&lt;/b&gt;으로 단축되었다. 최초 45분과 비교하면 &lt;b&gt;98% 이상의 성능 개선&lt;/b&gt;을 이룬 것이다. 시스템 자원 사용량은 극적으로 줄었고, 매일 아침 우리를 괴롭히던 45분의 유령은 마침내 한 줌의 재도 남기지 않고 완전히 소멸했다. 이 개선은 단순히 빨라진 것을 넘어, 시스템의 안정성과 신뢰도를 높이고 잠재적인 운영 리스크를 제거하는, 측정 가능한 비즈니스 가치를 창출했다.&lt;/p&gt;
&lt;table style=&quot;height: 139px;&quot; width=&quot;861&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;접근 방식&lt;/th&gt;
&lt;th&gt;사용 기술&lt;/th&gt;
&lt;th&gt;처리 시간&lt;/th&gt;
&lt;th&gt;개선율&lt;/th&gt;
&lt;th&gt;누적 개선율&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;초기 상태&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;해당 없음&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;단일 스레드 풀 테이블 스캔&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;약 45분&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;-&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;1단계&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;병렬 처리&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Java Fork/Join Framework&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;약 10분&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;약 78%&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;약 78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;2단계&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;근본적 DB 튜닝&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;복합 인덱스 (Composite Indexes)&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;1분 미만&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;1단계 대비 90% 이상&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;초기 대비 98% 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;결론: 단순함과 장인정신에 대한 교훈&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 길었던 유령 퇴치 작업의 끝에서, 나는 몇 가지 중요한 교훈을 얻었다. 45분에서 10분으로의 단축은 &lt;b&gt;노력의 승리&lt;/b&gt;였다. 손에 잡히는 '쉬운(easy)' 도구를 기민하게 적용하여 가시적인 성과를 냈고, 팀에 새로운 활력을 불어넣었다. 하지만 10분에서 1분 미만으로의 단축은 &lt;b&gt;사고(思考)의 승리&lt;/b&gt;였다. 문제의 본질을 파고들어 얽혀있지 않은 '단순한(simple)' 해법을 찾아냈기 때문이다. 첫 번째 해결책이 울퉁불퉁한 길을 더 빨리 달리게 해주는 강력한 엔진을 단 것이었다면, 두 번째 해결책은 길 자체를 매끄럽게 포장하는 작업에 가까웠다. 더 이상 강력한 엔진이 필요 없을 정도로 말이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 경험은 소프트웨어 공학의 세계에서 가장 화려하고 복잡한 기술이 항상 최고의 해결책은 아님을 다시 한번 각인시켜 주었다. 데이터베이스가 실제로 어떻게 데이터를 읽고 쓰는가에 대한, 어찌 보면 고루하게 느껴질 수 있는 근본 원리의 이해는, 최신 프레임워크를 피상적으로 적용하는 것보다 훨씬 더 강력한 힘을 발휘한다. 마치 유행하는 요리법을 수십 개 아는 것보다, 소금과 불을 다루는 기본기를 마스터한 요리사가 결국 더 훌륭한 음식을 만드는 것과 같은 이치다. 우리가 적용한 복합 인덱스는 수십 년 된 기술이었지만, 문제의 핵심을 정확히 겨눴기에 그 어떤 최신 병렬 처리 기술보다 우아하고 효과적이었다. 이는 &quot;단순하게, 바보야(Keep it simple, stupid)&quot;라는 KISS 원칙의 정수를 보여준다. 가장 아름다운 해결책은 종종 가장 적은 부품으로, 가장 조용하게 작동한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;진정한 엔지니어링의 탁월함은 주어진 업무를 완수하는 데 그치지 않는다. 그것은 &quot;왜?&quot;라고 질문하는 지적 호기심과, '이만하면 됐다'는 안일한 타협점을 넘어 문제의 진정한 해결에 도달할 때까지 파고드는 주인의식(Ownership)에서 비롯된다. 이는 단순히 코드를 작성하는 '코더'와 시스템의 건강을 책임지는 '엔지니어'를 가르는 결정적 차이다. 컴퓨터 과학의 거장 도널드 크누스(Donald Knuth)는 &quot;최고의 프로그램은 기계가 빠르게 실행할 수 있도록, 그리고 동시에 인간이 명확하게 이해할 수 있도록 작성된 것&quot;이라고 말했다. 우리의 두 번째 해법은 기계의 부담을 극적으로 줄였을 뿐만 아니라, 그 논리가 데이터의 본질적 패턴에 명확히 기대고 있기에, 훗날 이 시스템을 마주할 다른 개발자가 이해하기에도 훨씬 쉬운, 좋은 프로그램에 더 가까워졌다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로, 이 모든 과정은 기술만으로는 결코 완성될 수 없었다는 점을 강조하고 싶다. 후배 개발자에게는 스스로의 힘으로 문제를 분석하고, 때로는 실패하더라도 다시 도전하며, 마침내 성공의 기쁨을 맛볼 수 있는 안전한 환경을 제공하는 멘토십이 있었다. 또한, 데이터베이스 관리자와의 관계는 이 이야기의 숨은 주인공이다. 첫 번째 대화가 전문 영역이라는 보이지 않는 벽을 사이에 둔 요청과 거절이었다면, 두 번째 대화는 각자의 전문성을 존중하며 공동의 목표를 향해 나아가는, 진정한 협업의 시작이었다. 이는 개발자가 데이터베이스의 원리를 이해하고 DBA의 언어로 소통할 때, 그들이 더 이상 문지기와 침입자가 아닌, 시스템이라는 배를 함께 항해하는 동료가 될 수 있음을 보여주는 작은 데브옵스(DevOps)의 실천 사례이기도 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;결국 45분의 유령은 은 탄환으로 잡아야 할 괴물이 아니었다. 그것은 우리에게 기술 부채의 이자가 얼마나 무서운지, 무차별 대입의 한계가 어디까지인지, 잘 설계된 인덱스 하나가 얼마나 조용하고 강력한 힘을 발휘하는지, 그리고 복잡한 세상 속에서 단순함을 추구하는 것이 얼마나 가치 있는 일인지를 가르쳐준, 고마운 스승이었다. 그 유령 덕분에 우리는 더 나은 엔지니어가 되었고, 우리 팀은 더 단단해졌다.&lt;/p&gt;</description>
      <category>에세이</category>
      <author>북항</author>
      <guid isPermaLink="true">https://webfirewood.tistory.com/158</guid>
      <comments>https://webfirewood.tistory.com/158#entry158comment</comments>
      <pubDate>Tue, 25 Apr 2023 15:00:21 +0900</pubDate>
    </item>
    <item>
      <title>낡은 지도를 들고 미지의 도시를 탐험하는 법</title>
      <link>https://webfirewood.tistory.com/155</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론: 고장 난 나침반이 가리키는 곳&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공적인 비즈니스 시스템은 종종 잘 계획된 도시와 비견되곤 한다. 초창기에는 명확한 청사진 아래 효율적으로 성장하지만, 세월이 흐르며 증축과 개축이 반복되고 초기 설계 사상은 점차 희미해진다. 누구도 모르는 지름길과 기록에 없는 골목이 생겨나고, 도시의 중요한 조례들은 이제는 잊힌 방언으로 쓰인 고문서처럼 변해간다. 결국 도시는 거대한 미궁, 즉 누구도 전체 구조를 파악하기 힘든 레거시 시스템이 되고 만다. 이러한 시스템은 비즈니스 확장을 저해하고, 혁신을 늦추며, 고객의 불만을 먹고 자라는 심각한 제약으로 작용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 몸담았던 회사는 소프트웨어를 팔았지만, 그 방식이 조금 독특했다. 마치 일류 셰프가 자신의 요리뿐 아니라 그 요리를 담는 특별 제작 그릇까지 함께 팔아야 직성이 풀리는 것처럼, 우리는 교육용 앱이 설치된 물리적인 태블릿 기기를 함께 판매했다. 이 &amp;lsquo;1계약:1패드&amp;rsquo;라는 비즈니스 모델은 초창기에는 통제된 환경을 제공하는 견고한 &amp;lsquo;성곽 도시&amp;rsquo;처럼 보였을지 모른다. 하지만 도시가 번영하고 시민이 늘어날수록, 이 견고함은 성장의 발목을 잡는 &amp;lsquo;황금으로 만든 족쇄&amp;rsquo;가 되었다. 초기 비즈니스 모델의 성공이 역설적으로 장기적인 기술 부채로 이어진다는 사실은 주목할 만하다. 단기적인 사업 요구에 집중한 초기 설계가 장기적인 확장성을 고려하지 못했고, 그 성공은 시스템의 근본적인 한계를 가리는 장막이 되어주었다. 나중에 해결하기 훨씬 더 어려운 심각한 기술 부채가 그 장막 뒤에서 조용히 쌓여가고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과는 고객의 책상 위에서 기묘한 형태로 나타났다. 한 고객이 여러 학습 서비스를 구매할 때마다, 서비스 숫자만큼 태블릿이 하나씩 늘어가는 불편함, 21세기의 디지털 경험과는 동떨어진 이 현실은 고객센터의 전화기를 쉴 새 없이 울렸다. 고객의 목소리(VOC)는 더 이상 단순한 데이터가 아니었다. 그것은 낡은 도시의 비효율에 갇힌 시민들이 보내는 구조 신호였고, 개선을 향한 거대한 합창이었다. 혁신에 쓰여야 할 IT 예산의 상당 부분이 이 부채의 이자를 갚는 데, 즉 낡은 시스템을 겨우 유지하고 지탱하는 데 소모되고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침내 그 합창은 더는 외면할 수 없는 거대한 노크 소리가 되어 우리 개발팀의 문을 두드렸다. 내가 팀에 합류한 뒤 처음으로 맡게 된 이 대규모 프로젝트는 그렇게 시작되었다. 우리 손에 들린 것은 목적지는 명확히 가리키고 있지만, 그곳까지 가는 길은 알려주지 않는 &amp;lsquo;고장 난 나침반&amp;rsquo;과도 같았다. 우리의 과제는 이 낡은 도시의 비효율적인 교통 시스템을 근본적으로 재설계하고, 시민들이 도시 안에서 자유롭게 이동할 수 있는 새로운 길을 내는 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 고대의 암호문, 오라클 프로시저와의 조우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 심장부를 해부하자, 그곳에는 수천 줄에 달하는 거대한 오라클 저장 프로시저가 똬리를 틀고 있었다. 시스템의 핵심 인증과 계약 정보를 처리하는 이 프로시저는, 한때 안정성의 상징이었을지 모르나 이제는 모든 변화를 가로막는 거대한 문지기가 되어 있었다. 그 실체는 &amp;lsquo;스파게티 코드&amp;rsquo;라는, 무질서하게 엉킨 혈전 덩어리와 같았다. 수많은 개발자들이 십수년에 걸쳐 남긴 코드 조각들이 문서도, 일관된 규칙도 없이 뒤엉켜 있었으며, 시스템의 작동 원리를 알던 &amp;lsquo;원로&amp;rsquo; 개발자들은 대부분 은퇴했거나 회사를 떠난 지 오래였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 복잡성은 증상일 뿐, 병의 근원은 더 깊은 곳에 있었다. 바로 &amp;lsquo;객체-관계 임피던스 불일치(Object-Relational Impedance Mismatch)&amp;rsquo;라는, 소프트웨어 공학의 오래된 난제였다. 이 거창한 이름이 가리키는 것은, 세상을 바라보는 두 개의 근본적으로 다른 세계관 사이의 충돌이다. 하나는 데이터를 행위와 함께 캡슐화하는 객체 지향 프로그래밍의 세계관이고, 다른 하나는 데이터를 정규화된 테이블의 집합으로 보고 선언적인 언어(SQL)로 다루는 관계형 데이터베이스의 세계관이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 시스템은 이 근본적인 충돌을 어색하게 봉합한 결과물이었다. 애플리케이션의 &amp;lsquo;행위&amp;rsquo;에 해당하는 복잡한 비즈니스 로직이, 데이터 저장소인 오라클 프로시저 내부에 갇혀 있었던 것이었다. 이는 자동차의 엔진 제어 로직을 연료 탱크 안에 설계한 것과 같았다. 로직을 수정하거나 테스트하는 것은 고사하고, 버전 관리, 자동화된 테스트, CI/CD 파이프라인 같은 현대적 개발 생명주기에 편입시키는 것 자체가 불가능했다. 비즈니스 규칙의 변경이 필요할 때마다 개발 라이프사이클을 우회하여 데이터베이스를 직접 수정해야 하는 상황은 변경에 대한 저항성을 극도로 높였고, 급변하는 시장에 대한 비즈니스의 신속한 대응 능력을 심각하게 저하시켰다. 기술적 제약이 비즈니스 혁신의 발목을 잡는 핵심 요인이 된 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 기술적 복잡성은 개발팀에 막대한 인간적 비용을 초래했다. 디버깅이 &amp;lsquo;고고학적 발굴&amp;rsquo;에 비유될 정도로 시간과 노력이 소모된다는 것은, 개발자의 좌절감과 번아웃으로 이어질 수 있음을 의미했다. 우리의 첫 과제는 이 낡고 녹슨 금고를 깨부수고 그 안에 화석처럼 갇혀 있던 비즈니스 로직을 해방시켜, 마땅히 있어야 할 유연하고 투명한 애플리케이션의 품으로 돌려보내는 일이었다. 그것은 단순한 리팩토링을 넘어, 시스템의 근본적인 체질을 개선하는 대수술의 시작이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 점진적 혁명: 교살자 무화과와 배관공의 철학&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 복잡하고 위태로운 레거시 시스템을 현대화하는 과업 앞에서, 팀은 두 가지 상반된 길 중 하나를 선택해야 했다. 하나는 기존 시스템을 한 번에 전면적으로 교체하는 &amp;lsquo;빅뱅(Big Bang)&amp;rsquo; 방식이다. 이는 낡은 도시를 통째로 허물고 하루아침에 새로운 스마트 시티를 세우겠다는, 유혹적이지만 무모한 계획과도 같았다. 업계의 통계는 이러한 대규모 데이터 마이그레이션 프로젝트의 80% 이상이 예산이나 기간을 초과하며, 그 위험성은 시스템의 복잡성에 비례하여 기하급수적으로 증가한다고 경고한다. 핵심 비즈니스 로직을 다루는 이번 프로젝트에서 빅뱅 방식은 사실상 실패 확률이 높은 도박과 같았고, 만에 하나 실패할 경우 비즈니스 전체를 마비시킬 수 있는 잠재적 재앙이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 위험을 회피하기 위해, 우리는 마틴 파울러(Martin Fowler)에 의해 널리 알려진 &amp;lsquo;교살자 무화과 패턴(Strangler Fig Pattern)&amp;rsquo;이라는, 훨씬 더 신중하고 외과적인 접근법을 채택했다. 이 패턴의 이름은 숙주 나무의 가지에서 싹을 틔워, 서서히 뿌리를 내리며 숙주를 감싸고 자라나 결국에는 숙주 나무를 완전히 대체하고 그 자리에 새로운 나무로 우뚝 서는 교살자 무화과(Strangler Fig)의 생장 방식에서 유래했다. 소프트웨어의 맥락에서 이는 기존 레거시 시스템이라는 늙고 병든 나무를 그대로 살려둔 채, 그 주변부부터 새로운 기능이라는 건강한 덩굴을 하나씩 뻗어 나가게 하는 점진적 교체 전략을 의미한다. 새로운 시스템이 점차 자라나 기존 시스템의 기능을 하나씩 대체함에 따라, 낡은 시스템은 서서히 그 역할을 잃고 최종적으로는 누구도 눈치채지 못하는 사이에 조용히, 그리고 안전하게 은퇴(decommission)하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHImhB/btsPE4xDH0N/Fj3Djqnxt7QOB83fVoBQwk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHImhB/btsPE4xDH0N/Fj3Djqnxt7QOB83fVoBQwk/img.jpg&quot; data-alt=&quot;우리팀은 교살자 무화과 패턴을 적용해 달콤한 무화과를 수확할 수 있었을까?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHImhB/btsPE4xDH0N/Fj3Djqnxt7QOB83fVoBQwk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHImhB%2FbtsPE4xDH0N%2FFj3Djqnxt7QOB83fVoBQwk%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;1280&quot; height=&quot;830&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;830&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;이 전략의 기술적 심장부에는 &amp;lsquo;퍼사드(Facade)&amp;rsquo; 또는 &amp;lsquo;프록시(Proxy)&amp;rsquo; 역할을 하는 지능적인 라우팅 메커니즘이 있었다. 우리는 도시로 들어오는 모든 요청을 받는 새로운 정문을 세웠다. 이 지능적인 정문지기는 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;더 나아가 이 라우팅 계층은 낡은 세계와 새로운 세계 사이의 번역가이자 방화벽 역할을 하는 &amp;lsquo;안티-코럽션 레이어(Anti-Corruption Layer)&amp;rsquo;로서 기능했다. 이는 마치 서로 다른 언어와 도량형을 사용하는 두 나라의 국경에 세워진 정교한 세관과도 같았다. 레거시 시스템의 낡은 데이터 모델이나 불합리한 관습들이 새로 건설되는 깨끗한 도시로 &amp;lsquo;감염&amp;rsquo;되는 것을 막아주는 전략적인 방어선이었다. 이 방화벽 덕분에 새로 개발되는 코드베이스는 레거시의 문제점을 답습하지 않고, 진정으로 현대적이고 유지보수 가능한 자산으로 구축될 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴 적용의 첫 단계이자 가장 어려운 관문은 교체할 기능 단위, 즉 &amp;lsquo;얇은 조각(thin slices)&amp;rsquo;을 식별하는 것이었다. 거대한 모놀리식 프로시저의 어느 부분을 첫 번째 수술 부위로 정할 것인가? 놀랍게도 그 해답은 레거시 코드 자체의 복잡성 속에 숨어 있었다. 프로시저 내부에 거미줄처럼 얽혀 있던 수많은 IF/CASE 분기문들이 바로 그것이었다. 이 분기문들은 각기 다른 비즈니스 시나리오나 정책을 처리하는 논리적 경계선 역할을 하고 있었다. 즉, 레거시 코드의 복잡한 구조 자체가 역설적으로 점진적 분해를 위한 로드맵을 제공한 셈이었다. 우리의 작업은 단순한 코드 변환을 넘어선 역공학적 탐사, 즉 &amp;lsquo;소프트웨어 고고학&amp;rsquo;에 가까웠다. 낡은 지도의 해독 불가능해 보였던 기호들 속에서, 오히려 새로운 도시를 건설할 구획도를 발견해낸 것이었다. 이는 낡은 수도관을 교체하기 위해 집 전체를 부수는 대신, 기존 설계도를 면밀히 분석하여 가장 문제가 되는 부분부터 하나씩 신중하게 교체해 나가는 숙련된 배관공의 철학과도 같았다.&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;3. 신뢰 구축의 기술: 그림자 속의 리허설과 실용주의적 타협&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 비즈니스 로직을 교체하는 작업은 마치 살아있는 환자의 대동맥을 교체하는 대수술과 같았다. 이러한 수술에서 &amp;lsquo;거의 정확하다&amp;rsquo;는 말은 &amp;lsquo;완전히 틀렸다&amp;rsquo;는 말과 동의어이며, 작은 실수 하나가 치명적인 결과로 이어질 수 있다. 낡은 수도관을 교체하는 동안 도시 전체의 물 공급을 끊을 수 없듯, 우리는 시스템의 심장을 교체하면서도 서비스의 맥박이 단 한 순간도 멈추는 것을 허용할 수 없었다. 이를 위해 우리는 &amp;lsquo;점진적 전환과 병렬 검증&amp;rsquo;이라는, 수술팀을 위한 정교한 비행 시뮬레이터와도 같은 안전장치를 고안했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전략은 업계에서 &amp;lsquo;병렬 실행(Parallel Run)&amp;rsquo; 또는 &amp;lsquo;섀도우 모드(Shadow Mode)&amp;rsquo;로 알려진 기법으로, 새로운 시스템이 실전에 투입되기 전에 실제 환경에서 구 시스템과 동일하게 작동함을 100% 확신하기 위한 강력한 방법론이다. 개념은 간단하지만 실행은 정교했다. 패드 인증과 관련된 API 요청이 들어오면, 애플리케이션은 내부적으로 두 갈래의 작업을 동시에 수행했다. 하나는 수십 년간 안정성을 검증받은 기존의 오라클 저장 프로시저를 호출하는 것이고, 다른 하나는 새로 개발된 자바(Java) 기반의 비즈니스 로직을 실행하는 것이었다. 이 두 로직이 각각의 결과를 반환하면, 시스템 내의 엄격한 &amp;lsquo;심판&amp;rsquo;은 두 결과값이 원자적 수준에서 완전히 일치하는지 비교하는 검증 로직을 수행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 두 결과 사이에 사소한 차이라도 발견되면, 시스템은 즉시 상세한 컨텍스트(입력 파라미터, 실행 시각, 두 결과값 등)와 함께 불일치 로그를 기록했다. 이 로그는 개발팀에게 무엇보다 귀중한 &amp;lsquo;무료 실제 테스트 케이스&amp;rsquo;가 되었다. 우리가 미처 생각하지 못했던 예외 상황이나 코드 속에 화석처럼 박혀 있던 숨겨진 비즈니스 규칙을 알려주는 귀중한 교훈이었다. 이 과정을 통해 낡은 레거시 시스템은 새로운 시스템에게 자신의 모든 지혜와 비밀을 가르치는 &amp;lsquo;스승&amp;rsquo;의 역할을 했다. 가장 중요한 점은, 비교 결과와 상관없이 사용자에게 최종적으로 반환되는 응답은 항상 기존의 안정적인 오라클 프로시저가 생성한 결과였다는 것이다. 새로운 자바 로직은 오직 &amp;lsquo;그림자&amp;rsquo;처럼 존재하며 학습하고 검증받을 뿐, 실제 서비스에는 아무런 영향을 미치지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전략은 &amp;lsquo;배포(Deployment)&amp;rsquo;와 &amp;lsquo;출시(Release)&amp;rsquo;라는 두 개념을 의도적으로 분리하는, 정교한 위험 관리 기법이기도 했다. 새로운 코드는 사용자에게 영향을 주지 않는 상태로 운영 환경에 &amp;lsquo;배포&amp;rsquo;되어, 수많은 실제 요청들을 처리하며 스스로의 완벽함을 데이터로 증명해 나갔다. 그리고 마침내 새로운 로직이 기존 로직과 100% 동일하게 작동한다는 확신이 섰을 때, &amp;lsquo;출시&amp;rsquo;는 단지 설정 스위치 하나를 켜는(기존 로직 대신 새로운 로직의 결과를 반환하도록 변경하는) 지극히 간단하고 안전한 작업이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한편, 이처럼 안전한 전환의 발판을 마련한 뒤에도 현실적인 난관은 남아있었다. 팀의 숙원이었던 JPA 도입은 기존 코드와의 철학적 충돌이라는 또 다른 과제를 안겨주었다. 기존 데이터 접근 로직은 성능을 위해 여러 테이블을 복잡하게 조인하여 한 번에 모든 것을 가져오는, 소위 &amp;lsquo;한방 쿼리&amp;rsquo; 스타일의 MyBatis로 가득했다. 이는 마치 도시의 모든 정보를 얻기 위해 기록 보관소의 모든 관련 서류를 단 한 번의 방문으로 몽땅 꺼내오는 방식과 같았다. 반면, 객체 간의 관계와 그래프 탐색을 중시하는 JPA의 철학은 필요한 정보만을 따라 우아하게 탐색하는 방식에 가까웠다. 이 &amp;lsquo;한방 쿼리&amp;rsquo;를 JPA로 어설프게 구현하려다가는, 연관된 데이터를 조회하기 위해 수많은 추가 쿼리가 발생하는 악명 높은 &amp;lsquo;N+1 쿼리 문제&amp;rsquo;라는 함정에 빠지기 십상이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 이 기술적 간극 앞에서 원칙론자가 되기보다는 실용주의적인 배관공이 되기로 했다. 성능이라는 대원칙 아래, 상황에 맞는 최적의 도구를 선택하는 하이브리드 전략을 택한 것이다. 비교적 구조가 명확하고 단순한 조회는 JPA의 JPQL을 활용하여 높은 생산성과 가독성을 확보했다. 하지만 여러 도메인의 정보가 복잡하게 얽힌 &amp;lsquo;한방 쿼리&amp;rsquo;의 경우에는, 각 도메인 모델(예: 고객, 계약, 상품)을 개별적으로 조회한 뒤 애플리케이션단에서 이를 조합하고 가공하는 방식을 취했다. 때로는 이것이 객체지향의 순수성을 조금 해치는 것처럼 보일지라도, 사용자를 기다리게 하는 것보다는 현명한 타협이라 판단했다. 이처럼 기술적 이상과 현실적 제약 사이의 균형점을 찾는 과정은, 레거시 현대화가 단순히 새로운 기술을 적용하는 것을 넘어, 주어진 상황 속에서 가장 합리적인 가치를 창출하는 지혜로운 의사결정의 연속임을 가르쳐주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론: 숫자를 넘어선 보람, 그리고 남겨진 것들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 성공적으로 끝났다. 수술은 끝났고, 환자는 건강하게 회복했다. 도시의 교통 시스템을 마비시키던 근본 원인은 해결되었고, 시민들의 불편은 눈에 띄게 줄었다. 고객센터를 울리던 전화벨 소리는 잦아들었고, 고객 불만(VOC) 수치는 전월 대비 10% 감소라는 명확한 데이터로 보답했다. 시스템 내부적으로는 코드 커버리지가 20% 이상 향상되고 장애 대응 시간이 40% 단축되는 등, 보이지 않는 곳의 건강성도 극적으로 개선되었다. 우리는 낡은 지도를 들고 시작한 탐험을, 단 한 건의 장애 없이 안전하게 완수했다.&lt;/p&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;라며 기꺼이 신뢰라는 지지대를 내어주었다. 그 믿음은 내가 가장 합리적인 해결책을 찾아 나설 용기를 주었고, 스스로의 판단에 대한 확신을 되찾게 해주었다. 기술적 난제에 대해 밤늦도록 토론하고, 마침내 우아한 해결책을 찾았을 때 함께 기뻐하던 순간들. &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;물론 모든 이야기가 그렇듯, 현실에는 언제나 약간의 그림자가 드리워져 있었다. 우리의 성과는 직접적인 매출 증대와 같은 화려한 지표로 나타나지 않았다. 우리는 도시의 새로운 랜드마크를 건설한 것이 아니라, 눈에 보이지 않는 낡은 하수관을 교체한 것에 가까웠기 때문이다. 그 결과, 프로젝트는 경영진의 화려한 스포트라이트를 받지는 못했다. 그리고 가장 큰 아쉬움은, 최고의 합을 자랑하며 이 험난한 여정을 함께했던 팀이 프로젝트 종료 후 조직 개편이라는 현실의 파도 속에서 각자의 길을 가게 된 것이었다. 성공적인 수술을 마친 외과팀이 병원의 사정으로 해체된 것과도 같은, 씁쓸한 뒷맛이 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 나는 이 프로젝트의 진정한 성공이 외부의 평가나 숫자에 있지 않다고 믿는다. 그 가치는 고객의 오랜 고통을 해결했다는 뚜렷한 엔지니어링의 본질, 동료들의 깊은 신뢰 속에서 잃었던 자존감을 온전히 되찾은 개인적 성장, 그리고 다음 세대의 동료들을 위해 낡고 위험한 시스템을 더 나은 상태로 물려주었다는 장인으로서의 소박한 보람에 있다. 기술 부채의 진정한 비용은 그것을 해결하는 데 드는 직접적인 비용이 아니라, 혁신의 기회를 상실하면서 복리로 쌓이는 &amp;lsquo;기회비용&amp;rsquo;이다. 우리는 그 기회비용의 고리를 끊어냈고, 미래의 혁신을 위한 단단한 발판을 마련했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 하나의 &amp;lsquo;도가니&amp;rsquo;였다. 수년간의 마이그레이션을 성공적으로 마친 팀은 시작할 때와는 다른 팀이 되었다. 우리는 새로운 기술, 새로운 규율(TDD, 점진주의), 그리고 기술 부채의 비용과 품질의 가치에 대한 새로운 공유된 이해를 갖게 되었다. 비록 지금은 흩어졌지만, 그 시절 낡은 수도관을 함께 교체하며 땀 흘렸던 동료들과의 기억, 그리고 그 과정에서 체득한 문화적 유산은 내게 무엇과도 바꿀 수 없는 귀한 자산으로 남아있다. 그것이 바로 이 프로젝트가 내게 남긴, 숫자를 넘어선 진정한 보상이다.&lt;/p&gt;</description>
      <category>에세이</category>
      <author>북항</author>
      <guid isPermaLink="true">https://webfirewood.tistory.com/155</guid>
      <comments>https://webfirewood.tistory.com/155#entry155comment</comments>
      <pubDate>Thu, 16 Mar 2023 10:41:50 +0900</pubDate>
    </item>
    <item>
      <title>구명조끼와 위조된 보물 지도: 오해와 삽질의 연대기</title>
      <link>https://webfirewood.tistory.com/144</link>
      <description>&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;뜻밖의 지명: 위기라는 바다에 던져진 구명조끼&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;전 세계가 숨을 죽였던 2020년 말, 내가 몸담고 있던 이커머스 기업의 물류 부문은 인류의 활동이 멈춘 세상에서 유일하게 가속 페달을 밟고 있는 듯했다. 팬데믹은 더 이상 먼 나라의 뉴스가 아니었다. 그것은 매일 수만 명의 직원이 드나드는 전국 수십 개의 물류센터 동맥을 끊어버릴 수 있는, 코앞에 닥친 실존적 위기였다. 전시 상황실을 방불케 하는 경영진 회의실에서 내려온 지시는 단순한 업무 지시가 아니라, 거의 물리 법칙에 가까운 절대적이고 시급한 명령이었다. &quot;전국에 흩어진 무류센터에서, 수천 명에 달하는 인원의 건강 상태와 출입동선을 실시간으로 확인하고 통제하는 비대면 시스템을 구축하라. '지금당장'.&quot;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZBrbE/btsPB4LVGRc/4wCIFcY4ztnQVwkgBmuNfK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZBrbE/btsPB4LVGRc/4wCIFcY4ztnQVwkgBmuNfK/img.jpg&quot; data-alt=&quot;물류센터는 팬데믹기간에 오히려 초고속으로 가속 페달을 밟고 있었다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZBrbE/btsPB4LVGRc/4wCIFcY4ztnQVwkgBmuNfK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZBrbE%2FbtsPB4LVGRc%2F4wCIFcY4ztnQVwkgBmuNfK%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;740&quot; height=&quot;494&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;494&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;물류센터는 팬데믹기간에 오히려 초고속으로 가속 페달을 밟고 있었다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;긴급히 소집된 팀 회의실의 공기는 압축된 산소처럼 팽팽했다. 언제나처럼 유능하고 책임감 넘치는 팀장님은 이 중차대한 임무를 맡겠다며 기꺼이 자원했다. 그는 수많은 시스템 장애와 출시 마감일의 포화 속에서 단련된 베테랑 지휘관이었지만, 그의 시선이 회의실을 한 바퀴 돌았을 때, 그의 얼굴에는 찰나의 곤혹감이 스쳤다. 팀원 모두가 각자의 '디지털 참호' 속에서 기존 서비스의 안정성을 지키기 위해 사투를 벌이고 있었다. 모두가 핵심 업무라는 보이지 않는 무게에 짓눌려, 이미 정신적&amp;middot;물리적 한계에 다다른 상태였던 것이다. 바로 그때, 회의에 참석했다기보다는 거의 관람에 가까운 태도로 어색하게 서 있던 한 주니어 개발자에게 모두의 시선이 꽂혔다. 바로 나였다. 나는 거대한 폭풍의 눈 한가운데로, 마치 잘못 배달된 소포처럼 덩그러니 놓여 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;솔직히 말해, 프로젝트의 무게를 감당하기에 내 경험은 턱없이 부족했다. 내 머릿속에서는 '나는 자격이 없어', '저건 거인들의 일이야'라는 '회피'의 사이렌이 요란하게 울리고 있었다. 하지만 진정한 리더십의 위대함은 위임의 기술에서 드러나는 법. 팀장님은 내게 일을 무책임하게 '던지지' 않았다. 대신, 성공을 위한 '스타터 키트'를 쥐여주었다. 그는 화이트보드로 성큼성큼 걸어가더니, 복잡한 요구사항을 한눈에 파악할 수 있는 간결한 시퀀스 다이어그램을 그려주었다. 키오스크에서 시작된 요청이 서버를 거쳐 어떻게 처리되어야 하는지, 그 과정에서 어떤 데이터가 오고 가야 하는지에 대한 핵심 흐름도였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그의 마커는 아키텍처가 아닌 '사용자 여정'을 그리고 있었다. &quot;여기서 QR을 찍으면, 이 정보를 받아 처리할 API가 필요하겠지. 체온 정보도 마찬가지고. 이 두 정보를 합쳐서 출입 가능 여부를 알려주는 응답을 내려줘야 해.&quot; 그는 필요한 API 명세에 대한 초기 아이디어를 제시하며 내가 길을 잃지 않도록 훌륭한 가이드라인을 제공했다. '어떻게' 구현할지를 지시한 것이 아니라, '무엇을' 만들어야 하는지에 대한 명확한 청사진을 그려준 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;1310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pproo/btsPBgsMocq/46wRlc0tm1gY33esW3twiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pproo/btsPBgsMocq/46wRlc0tm1gY33esW3twiK/img.png&quot; data-alt=&quot;프로젝트 주요 로직의 시퀀스 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pproo/btsPBgsMocq/46wRlc0tm1gY33esW3twiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPproo%2FbtsPBgsMocq%2F46wRlc0tm1gY33esW3twiK%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;1082&quot; height=&quot;1310&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;1310&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로젝트 주요 로직의 시퀀스 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이것은 단순한 업무 지시가 아니었다. 그것은 불확실성 속에서 잠재력에 투자하는, 급진적인 신뢰의 표현이었다. 그는 기술적 선택에 담긴 전략적 트레이드오프를 결정하는 과제는 내게 남겨두었다. 그렇게 나는 거대한 위기가 만들어낸 기회의 한복판으로, 구명조끼뿐만 아니라 '냅킨 위에 그려진 보물 지도'까지 손에 쥔 채 깊은 바다에 던져졌다. 앞으로 몇 달간 겪게 될 일들은, 수년간의 경험을 압축적으로 체득해야 하는 혹독한 성장의 서막이었다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;아키텍처의 고백: 두려움으로 빚은 설계도&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트의 첫 번째 갈림길에서 나는 중대한 기술적 결정을 내려야 했다. 새로운 시스템을 위해 깨끗하고 독립된 새 서버 환경을 구축할 것인가, 아니면 기존에 안정적으로 운영되던 서비스 위에 기능을 덧붙일 것인가. 소프트웨어 공학 교과서 1장 1절에 나올 법한 해답은 명확했다. 새로운 마이크로서비스를 구축하는 것은 시스템의 책임을 명확히 분리하고, 미래의 확장성을 확보하며, 다른 서비스의 장애로부터 자유로워지는 가장 '올바른' 길이었다. 마치 도시 계획가에게 새로운 구역을 개발하라는 임무가 주어졌을 때, 낡은 건물에 증축을 하는 대신 최첨단 마천루를 설계하는 것과 같은 이치였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나는 사뭇 진지한 표정으로 팀장님께 보고했다. &quot;팀장님, 이 프로젝트는 팬데믹이라는 일시적이고 특수한 상황에 대응하기 위한 것입니다. 언제 종식될지 모르는 상황에서 새로운 서버를 구축하고 인프라 리소스를 할당하는 것은 장기적으로 비효율적일 수 있습니다. 특히 우리처럼 소규모 팀이 분산 시스템의 복잡성을 관리하는 것은 상당한 운영 오버헤드를 유발할 것입니다. 따라서 시장 출시 속도(Time-to-Market)를 극대화하고 초기 투자 비용을 최소화하기 위해, 이미 검증된 기존의 안정적인 서비스에 기능을 통합하는 것이 현 상황에서 가장 현명한 전략적 판단이라고 생각합니다.&quot; 내 보고는 꽤 그럴듯한 비즈니스 논리로 무장한 것처럼 들렸다. 모놀리식 아키텍처가 제공하는 '개발의 단순성'과 '신속한 배포'라는 장점을 적극적으로 활용하자는, 계산된 비즈니스 전략처럼 보였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 여기에는 내가 차마 말하지 못한 진실, 나의 '더럽고 작은 비밀'이 숨어 있었다. 그것은 바로 인프라 기술, 특히 현대적인 DevOps 환경에 대한 나의 깊은 무지와 자신감 부족이었다. 내 머릿속에서 '마이크로서비스'라는 단어는 곧바로 쿠버네티스와 젠킨스라는 거대한 산맥으로 이어졌다. 수십 줄에 달하는 YAML 설정 파일을 작성하여 Pod, Deployment, Service를 정의하고, &lt;code&gt;kubectl&lt;/code&gt; 명령어로 컨테이너 오케스트레이션의 복잡한 내부를 들여다봐야 한다는 생각은 그 자체로 압박이었다. 여기에 더해, &lt;code&gt;Jenkinsfile&lt;/code&gt;을 처음부터 작성하여 빌드, 테스트, 배포 단계를 엮어내는 CI/CD 파이프라인을 구축하는 과정은 프로젝트 자체의 복잡성보다 훨씬 더 큰 실존적 공포로 다가왔다. 그것은 마치 황무지에 홀로 떨어져 집을 짓고, 전기를 끌어오고, 상하수도를 연결해야 하는 막막함과 같았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;결국 나의 선택은 기술적 탁월함에 대한 고뇌가 아닌, 미지의 영역에 대한 원초적인 두려움에서 비롯된 것이었다. 나는 황무지에 새로 집을 짓는 대신, 가구와 가전, 심지어 넷플릭스 구독까지 완벽하게 갖춰진 친구의 아파트에 '디지털 더부살이'를 하는 쪽을 택한 셈이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 선택은 자연스럽게 기술 스택을 결정했다. Java, Spring Boot, MySQL. 이 기술들은 내가 심오한 고민 끝에 고른 최적의 도구가 아니었다. 그저 내가 더부살이하기로 한 서비스가 이미 사용하고 있던 '기본 옵션'이었을 뿐이다. 물론, Spring Boot의 자동 구성(Autoconfiguration) 기능처럼 복잡한 설정을 대신 처리해주는 편리함은 분명 매력적이었다. 하지만 그보다 더 큰 안정감을 준 것은, 이 모든 것을 배포하기 위한 쿠버네티스 클러스터와 젠킨스 파이프라인이 이미 그 자리에 존재하고 있었다는 사실이었다. 나는 새로운 YAML 파일과 씨름하거나 파이프라인 스크립트를 디버깅하는 대신, 기존의 잘 닦인 고속도로에 조용히 차를 올리기만 하면 되었다. 복잡한 인프라 문제를 회피함으로써 나의 제한된 '인지 부하(Cognitive Load)'를 비즈니스 로직에만 집중할 수 있게 해주는, 지극히 현실적인 타협이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;지금 돌이켜보면, 개발자의 개인적인 불안감이나 기술 격차와 같은 심리적 요인이 시스템 아키텍처라는 거대한 청사진에 얼마나 깊은 흔적을 남기는지 깨닫게 된다. 개인의 한계를 그럴듯한 비즈니스 논리로 포장해 보고했던 그 순간은, 어쩌면 한 주니어 개발자가 생존을 위해 터득한 최초의 프로페셔널한 '선의의 거짓말'이었을지도 모른다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;데이터 쓰나미와 구세주: 디지털 화이트보드 Redis&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;초기 설계의 가장 큰 산을 넘었다는 안도감은, 마치 정상에 오르자마자 반대편에서 몰려오는 거대한 해일을 목격한 등산객의 심정과도 같았다. 프로젝트는 이제 또 다른 차원의 도전에 직면했다. 전국에 흩어진 1,000개가 넘는 키오스크가 중앙 서버를 향해 마치 포효하듯 쉴 새 없이 데이터를 쏟아내기 시작한 것이다. 출입 직원의 QR 코드 스캔 정보, 실시간으로 측정된 체온 데이터 등은 단 몇 초, 길어야 몇 분간은 출입 통제에 극도로 중요했지만, 그 이후에는 디지털 폐지에 불과한, 완전히 무의미해지는 휘발성 정보였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 데이터의 본질을 이해하지 못하고 시스템의 공식 기록 저장소(System of Record)인 MySQL에 이 모든 것을 기록하려 드는 것은, 고속도로를 스쳐 지나가는 모든 자동차의 번호판을 화강암 기념비에 일일이 정으로 새기는 것과 같은 어리석은 짓이었다. MySQL은 ACID 원칙을 철저히 지키며 데이터의 무결성과 영속성을 보장하는, 우리 시스템의 믿음직한 금고였다. 하지만 이 금고는 단기 기억상실증에 걸린 금붕어의 하루치 기억까지 영원히 보관하도록 설계되지 않았다. 만약 이 쓰레기 데이터의 홍수를 그대로 MySQL로 흘려보냈다면, 데이터베이스는 무의미한 쓰기(Write)와 지우기(Delete) 작업의 무게를 견디지 못하고 순식간에 과부하에 걸릴 것이 뻔했다. 디스크 I/O는 병목 현상을 일으키고, 테이블 인덱스는 끊임없이 재조정되며 파편화될 것이며, 결국 시스템 전체의 심장을 멎게 할 수도 있는 명백한 재앙이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이때 내 어설픈 지식의 서랍 속에서 희미하게 빛나던 이름이 바로 Redis였다. Redis는 이 폭풍 같은 데이터를 잠시 기록했다가 뒤끝 없이 깔끔하게 지워주는, 고속의 '디지털 화이트보드' 역할을 완벽하게 수행할 수 있었다. MySQL이 모든 것을 디스크라는 영구적인 저장소에 기록하는 신중한 역사가라면, Redis는 모든 것을 RAM이라는 휘발성 메모리에 기록하는 번개처럼 빠른 속기사였다. 디스크 접근이 도서관 서고에서 책을 찾아오는 과정이라면, 메모리 접근은 책상 위 메모지를 바로 집어 드는 속도와 같았다. 이 속도의 차이는 단순히 '빠르다'는 수준을 넘어, 아키텍처의 근본적인 패러다임을 바꿀 수 있는 힘을 가졌다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;무엇보다 Redis의 가장 우아한 기능은 데이터에 유효기간(Time-To-Live, TTL)을 설정하는 능력이었다. &lt;code&gt;SETEX&lt;/code&gt;와 같은 원자적(atomic) 명령어를 사용하면, 데이터를 저장하는 동시에 &quot;이 데이터는 정확히 30초 후에 스스로 파괴될 것&quot;이라는 시한폭탄 타이머를 설정할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;# Redis에 30초 유효기간으로 데이터 저장 (설정과 만료시간 지정을 한번에)
SETEX temp_user_session:user123 30 '{&quot;temp&quot;:36.5, &quot;access_try&quot;:1}'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이것은 단순한 편의 기능을 넘어선 철학적인 해답이었다. 데이터의 생명주기를 데이터의 본질과 일치시키는 것. 30초 후에는 마치 처음부터 존재하지 않았던 것처럼, 아무런 흔적도, 관리의 부담도 남기지 않고 사라지는 것이다. 더 이상 쓸모없어진 데이터를 정리하기 위한 별도의 스케줄러나 배치 작업을 만들 필요가 없었다. Redis는 스스로 뒷정리를 하는 완벽한 파트너였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이것이야말로 내가 이 프로젝트에서 처음으로, 미지에 대한 두려움이 아닌 문제 해결을 위한 순수한 엔지니어링적 판단으로 주도적으로 도입한 기술이었다. Redis를 도입하면서 나는 더 깊은 깨달음을 얻었다. Redis는 단순히 빠른 캐시나 임시 저장소가 아니었다. 그것은 시스템의 아키텍처적 '결합도 완화(decoupling)'를 가능하게 하는 핵심적인 완충 장치, 즉 '데이터 충격 흡수 장치'였다. 키오스크에서 쏟아지는 예측 불가능한 트래픽의 충격을 Redis라는 스프링이 모두 흡수해주었기에, 그 뒤에 있는 MySQL은 자신이 가장 잘하는 일, 즉 중요하고 영속적인 데이터를 안전하게 지키는 임무에만 집중할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbhejD/btsPB3Gh1Lr/aiWcvULmH6tz7cHs6dfpbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbhejD/btsPB3Gh1Lr/aiWcvULmH6tz7cHs6dfpbk/img.png&quot; data-alt=&quot;Redis를 도임한 이후 데이터베이스 부하 분산&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbhejD/btsPB3Gh1Lr/aiWcvULmH6tz7cHs6dfpbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbhejD%2FbtsPB3Gh1Lr%2FaiWcvULmH6tz7cHs6dfpbk%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;400&quot; height=&quot;292&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis를 도임한 이후 데이터베이스 부하 분산&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;덕분에 시스템은 수많은 키오스크로부터 쏟아지는 요청의 쓰나미를 안정적으로 처리하며 데이터베이스의 부담을 극적으로 덜어주었다. 이 작은 성공은 내게 기술적 자신감이라는, 그 어떤 보상보다 값진 자양분이 되었다. 두려움에 기반한 방어적인 선택이 아닌, 문제의 본질을 파고들어 최적의 도구를 찾아낸 첫 번째 승리였다. 길고 어두웠던 불확실성의 밤에 처음으로 비친 새벽빛과도 같은 경험이었다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;소크라테스식 코드 리뷰와 행복한 오해&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트의 지적, 감정적 중심에는 한 선배 개발자와의 코드 리뷰 경험이 자리 잡고 있다. 그는 압도적인 실력과 따뜻한 인품을 겸비한, 동료의 성장을 진심으로 돕는 '멸종 위기종'에 가까운 희귀한 멘토였다. 그의 코드 리뷰는 단순한 오류 지적의 목록이 아니었다. 그것은 마치 플라톤의 대화편에 등장하는 소크라테스처럼, 정답을 알려주는 대신 끝없는 질문을 통해 스스로 진리에 도달하게 만드는, 깊은 사유의 여정이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;전면 재택근무로 인해 모든 소통이 차가운 텍스트로만 이루어지던 어느 날 오후, 마침내 그에게서 코드 리뷰가 도착했다. 메신저 알림이 울리는 순간, 나는 마치 대학 입시 합격 여부를 확인하는 수험생처럼 심호흡을 하고 링크를 클릭했다. 리뷰의 맨 위에는 &quot;참고 사항:&quot;이라는, 온화하기 그지없는 머리말이 달려 있었다. 하지만 공포에 질린 내 눈은 그 자비로운 단어를 가볍게 건너뛰고, 곧바로 본문에 박힌 철학적 질문들의 심연으로 추락했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;참고 사항:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이 키오스크 시스템의 &lt;b&gt;책임 경계(Boundary)&lt;/b&gt;는 어디까지라고 생각하시나요? 예를 들어, 물류센터의 운영 시간이나 위치 같은 메타 정보를 우리 서비스가 직접 데이터베이스에 저장하고 소유하는 것이 장기적으로 최선일까요?&lt;/li&gt;
&lt;li&gt;고민해볼 점: 외부 시스템(인사 DB)에서 직원 정보를 가져오는 현재 로직은 안정적이지만, 만약 인사 DB에 장애가 발생하면 우리 시스템은 어떻게 동작하게 될까요? &lt;b&gt;데이터 접근 전략&lt;/b&gt;에 대해 API 직접 호출, 주기적인 캐싱, 혹은 우리 쪽 DB에 복제하여 동기화하는 방식 사이의 트레이드오프를 고민해보면 좋을 것 같습니다.&lt;/li&gt;
&lt;li&gt;논의해볼 만한 주제: 지금은 단일 기능이지만, 만약 앞으로 '방문자용 임시 QR 발급 기능'이나 '차량 출입 통제 기능'이 추가된다면 현재의 &lt;b&gt;단일 서비스 구조&lt;/b&gt;가 유효할까요?&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;만약 우리가 같은 공간에 있었다면, 그의 부드러운 미소나 &quot;이건 그냥 제 생각일 뿐이니 부담 갖지 말아요&quot;라는 따뜻한 말투를 통해 이 질문들이 나의 성장을 위한 지적 자극제임을 즉시 알아차렸을 것이다. 하지만 물리적으로 고립된 채 차가운 모니터 화면 너머의 텍스트를 마주한 내게, 이 심오한 질문들은 '자네의 설계는 근본부터 글러먹었으니, 당장 갈아엎게'라는, 해고 통지서의 서문처럼 들렸다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그날부터 며칠간, 나는 패닉과 카페인에 의지해 스스로를 '강제 아키텍처 부트캠프'로 몰아넣었다. 내가 작성했던 코드를 모조리 해체하고, 선배가 던진 질문에 대한 완벽한 답을 찾기 위해 밤을 새워 코드를 다시 쌓아 올렸다. 그 고통스러운 과정은 아래 표와 같이 요약할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;선배의 질문 (소크라테스식 문답)&lt;/th&gt;
&lt;th&gt;선배의 진짜 의도 (성장을 위한 부드러운 제안)&lt;/th&gt;
&lt;th&gt;주니어의 왜곡된 해석 (해고를 암시하는 암호문)&lt;/th&gt;
&lt;th&gt;결과적으로 취한 행동 (광적인 재설계와 삽질)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;서비스의 &lt;b&gt;책임 경계&lt;/b&gt;는 어디까지일까요? 물류센터 정보를 우리가 소유하는 게 맞을까요?&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;도메인 주도 설계(DDD)&lt;/b&gt; 관점에서 '바운디드 컨텍스트(Bounded Context)'와 &lt;b&gt;'단일 진실 공급원(SSOT)'&lt;/b&gt; 원칙에 대한 고민을 유도.&lt;/td&gt;
&lt;td&gt;&quot;자네는 데이터 소유권 개념도 없나? 데이터를 엉뚱한 곳에 둬서 시스템 전체를 오염시켰군. 당장 제자리로 옮기게.&quot;&lt;/td&gt;
&lt;td&gt;물류센터 메타 정보를 저장하던 테이블을 삭제. 기존 '물류 서비스'에 API를 요청해 실시간으로 정보를 받아오도록 구조를 전면 변경. 이 과정에서 불필요한 네트워크 호출이 폭증.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;b&gt;데이터 접근 전략&lt;/b&gt;의 트레이드오프를 고민해보면 어떨까요?&quot;&lt;/td&gt;
&lt;td&gt;시스템의 &lt;b&gt;복원력(Resilience)&lt;/b&gt;과 &lt;b&gt;성능&lt;/b&gt; 사이의 균형점을 생각해보자는 제안. 외부 서비스 장애에 대비한 캐싱 전략(예: Cache-Aside 패턴)의 필요성 환기.&lt;/td&gt;
&lt;td&gt;&quot;자네가 짠 코드는 외부 시스템이 재채기만 해도 서버가 터지는 시한폭탄이야. 전부 고치게.&quot;&lt;/td&gt;
&lt;td&gt;Redis를 이용한 &lt;code&gt;Cache-Aside&lt;/code&gt; 패턴을 급조하여 구현. 하지만 데이터 동기화 주기를 잘못 설정해 '오래된 데이터(Stale Data)' 문제가 발생할 가능성을 만들고, 이를 해결하기 위해 더 복잡한 로직을 추가하는 악순환에 빠짐.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;미래의 기능 확장을 고려할 때 &lt;b&gt;단일 서비스 구조&lt;/b&gt;가 유효할까요?&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;단일 책임 원칙(SRP)&lt;/b&gt;에 기반하여, 미래의 확장성을 고려한 마이크로서비스로의 점진적 전환 가능성에 대한 생각의 씨앗을 심어주기 위함.&lt;/td&gt;
&lt;td&gt;&quot;자네는 한 치 앞도 못 보는군. 이 코드는 유지보수 불가능한 '빅 볼 오브 머드(Big Ball of Mud)'가 될 운명이야.&quot;&lt;/td&gt;
&lt;td&gt;아직 존재하지도 않는 '방문자 관리', '차량 관리' 기능을 위한 인터페이스와 추상 클래스를 미리 설계. 이는 명백한 &lt;b&gt;'과잉 설계(Over-engineering)'&lt;/b&gt;였으며, 코드의 복잡도만 기하급수적으로 높이는 결과를 낳음.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나중에 모든 것이 나의 처절한 오해였음을 알게 되었을 때의 허탈함은 이루 말할 수 없었다. 선배는 그저 &quot;나중에 시스템이 커지면 이런 점들도 생각해보면 더 좋을 거예요&quot;라는, 미래를 위한 조언을 건넸을 뿐이었다. 하지만 이 '행복한 사고' 덕분에 나는 이 프로젝트에서 가장 큰 배움을 얻었다. 그 깊은 오해는 내가 단순히 '동작하는 코드'를 넘어, 시스템 설계의 근본적인 '왜'를 고민하도록 강제했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;도메인 주도 설계, 단일 진실 공급원, 복원성을 위한 캐싱 전략, 단일 책임 원칙... 이 단어들은 더 이상 책 속에 박제된 딱딱한 개념이 아니었다. 그것은 내가 밤을 새워가며 코드를 지우고 다시 쓰기를 반복했던 처절한 사투의 흔적이자, 고통을 통해 내 머릿속에 각인된 살아있는 지혜가 되었다. 고통스러웠던 만큼 그 가르침은 깊고 영구적으로 내게 새겨졌다. 때로는 잘못 꿰어진 첫 단추를 풀기 위한 처절한 몸부림이 옷 전체를 더 튼튼하게 만드는 법이다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;JSON으로 대화하기: 국경을 넘는 공감의 언어&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;기술적 도전의 산을 넘자, 이번에는 인간적인 협업이라는 새로운 형태의 히말라야가 눈앞에 나타났다. 키오스크 하드웨어 및 펌웨어 개발은 중국에 있는 파트너사의 개발팀과 긴밀하게 협력해야 하는 과제였다. 영어가 유창하지 않았던 나는, 마치 갓 번역된 사용설명서처럼 어색하고 딱딱한 문장들을 메신저 창에 조심스럽게 입력하며 소통을 시도했다. 내 문장은 문법적으로는 완벽했을지 몰라도, 영혼은 빠져나간 미라와 같았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;문제는 여기서 시작됐다. 중국팀 리더는 나의 기계처럼 완벽한 문어체 영어를 보고, 내가 사실은 셰익스피어의 후예쯤 되는 네이티브 스피커라고 굳게 믿어버린 모양이었다. 그는 불쑥불쑥, 아무런 예고도 없이 영어로 전화를 걸어오기 시작했다. 내 모니터 화면에 그의 이름이 번쩍일 때마다, 나는 마치 치과 드릴 소리를 듣는 것처럼 온몸의 신경이 곤두섰다. 심장이 멎는다는 표현은 과장이 아니었다. 나는 &quot;회의 중이다&quot;, &quot;지금은 자리를 비웠다&quot;와 같은, 누가 봐도 어색한 핑계를 둘러대며 필사적으로 전화를 피했다. 이 소통의 교착 상태는 프로젝트의 발목을 잡는 보이지 않는 위협이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이대로는 안 되겠다고 생각한 나는, 전화벨의 공포를 생산성으로 승화시키기로 결심했다. 더 유창한 영어를 배우는 대신, 아예 영어가 필요 없는 소통 방식을 만들기로 한 것이다. 나의 목표는 명확했다. 다시는 그에게서 전화가 걸려오지 않도록, 이 문서를 보는 지구상의 그 어떤 개발자라도 단 하나의 의문도 생기지 않을 만큼 완벽하고, 상세하며, 친절한 기술 문서를 만드는 것이었다. 그것은 단순한 API 명세서가 아니었다. 그것은 두 나라 사이에 맺어지는 일종의 '기술적 평화 협정'이자, 오해의 안개를 걷어낼 '외교 문서'가 되어야 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나는 세계 최고의 API 문서를 자랑하는 스트라이프(Stripe)의 문서를 마음속의 '북극성'으로 삼았다. 개발자에 대한 깊은 공감과 이해를 바탕으로, 통합 과정의 모든 마찰을 제거하려는 그들의 철학을 내 문서에 담고 싶었다. 나는 단순한 정보의 나열을 넘어, 파트너사 개발자의 '경험'을 설계하기 시작했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;첫째, &lt;b&gt;'빠른 시작 가이드(Quickstart Guide)'&lt;/b&gt;를 제공했다. 복잡한 인증 절차나 설정 과정을 건너뛰고, 단 몇 분 안에 키오스크가 서버와 &quot;Hello, World!&quot;를 외치며 첫 교신에 성공하는 가장 빠른 경로를 제시했다. 첫 성공의 경험은 그들에게 자신감과 안정감을 줄 것이라 믿었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;둘째, 모든 엔드포인트와 파라미터를 설명하는 것을 넘어, &lt;b&gt;'왜(Why)'&lt;/b&gt;를 설명했다. 각 API가 어떤 비즈니스 맥락에서 필요한지, 각 파라미터가 시스템 전체의 흐름에서 어떤 역할을 하는지를 명시했다. 이는 그들이 단순히 기계적으로 코드를 복사-붙여넣기 하는 것을 넘어, 시스템의 의도를 이해하고 더 나은 코드를 작성하도록 도울 것이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;셋째, 그리고 가장 중요하게는, &lt;b&gt;JSON 예시의 향연&lt;/b&gt;을 펼쳤다. 나는 거의 집착에 가까울 정도로 모든 가능한 시나리오에 대한 요청과 응답 JSON 예시를 만들었다. 이것은 단순한 예제가 아니라, 한 편의 '상황별 시나리오 대본'이었다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;/*
  시나리오 1: 정상 출입 (성공)
  - 직원이 유효한 QR 코드를 스캔하고, 체온이 정상 범위일 때.
*/
// 요청 예시: 직원 QR 스캔 및 체온 측정
{
  &quot;kioskId&quot;: &quot;KIOSK_LOBBY_A_01&quot;,
  &quot;qrToken&quot;: &quot;a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6&quot;,
  &quot;temperature&quot;: 36.5,
  &quot;timestamp&quot;: &quot;2020-11-15T10:30:00Z&quot;
}

// 성공 응답 예시
{
  &quot;status&quot;: &quot;SUCCESS&quot;,
  &quot;data&quot;: {
    &quot;userName&quot;: &quot;홍길동&quot;,
    &quot;accessGranted&quot;: true,
    &quot;message&quot;: &quot;환영합니다. 출입이 허가되었습니다.&quot;
  },
  &quot;error&quot;: null
}

/*
  시나리오 2: 유효하지 않은 QR (실패)
  - 등록되지 않았거나 만료된 QR 코드를 스캔했을 때.
*/
// 실패 응답 예시: 잘못된 토큰
{
  &quot;status&quot;: &quot;FAIL&quot;,
  &quot;data&quot;: null,
  &quot;error&quot;: {
    &quot;errorCode&quot;: &quot;E401_INVALID_TOKEN&quot;,
    &quot;message&quot;: &quot;유효하지 않은 QR 코드입니다. 관리자에게 문의하세요.&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로, &lt;b&gt;'포괄적인 오류 코드 용어집'&lt;/b&gt;을 추가했다. &quot;E401_INVALID_TOKEN&quot;과 같은 모호한 코드만 던져주는 대신, 무엇이 잘못되었는지, 왜 이 에러가 발생했는지, 그리고 가장 중요하게는 이 문제를 해결하기 위해 무엇을 시도해볼 수 있는지를 명확하게 설명했다. 이것은 디버깅 과정에서 그들이 겪을 좌절감을 줄여주기 위한, 나의 기술적 공감의 표현이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;문서를 완성하여 공유한 뒤, 기적이 일어났다. 전화벨은 더 이상 울리지 않았다. 대신, 중국팀과의 소통은 간결한 텍스트와 코드, 그리고 내가 만든 문서의 특정 섹션을 가리키는 링크로 이루어졌다. 우리는 더 이상 어설픈 영어로 대화하지 않았다. 우리는 명확하고 오해의 소지가 없는 JSON으로 대화했다. 잘 정의된 JSON 구조는 그 자체로 국경과 언어, 문화의 장벽을 초월하는 가장 강력하고 효율적인 공통의 언어였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그때 깨달았다. 훌륭한 기술 문서는 단순히 정보를 전달하는 수단이 아니다. 그것은 상대방이 무엇을 궁금해할지, 어떤 부분에서 어려움을 겪을지 미리 예측하고 모든 답을 준비해두는, 기술의 형태를 띤 궁극의 '공감 행위'라는 것을. 전화벨에 대한 나의 공포가, 역설적으로 우리 팀과 파트너사 사이에 더 깊은 신뢰와 효율적인 협업의 다리를 놓은 셈이다. 이 경험을 통해 나는 언어에 대한 불안감을, 세계적 수준의 소통 도구를 만들어내는 강력한 원동력으로 전환시킬 수 있었다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;성장의 흉터: 나를 만든 것들&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트는 공식적으로 대성공을 거두었다. 시스템 도입 후 회사는 연간 10억 원 이상의 인건비를 절감했고, 물류센터의 안전과 운영 효율성은 극대화되었다. 나는 그 분기 우수 성과자로 선정되는 영광도 안았다. 이것이 이 프로젝트의 공식적인 대차대조표에 기록된 흑자이자, 나의 이력서에 남은 반짝이는 한 줄이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 눈부신 성공은 거대한 빙산의 일각에 불과하다. 진짜 이야기는 수면 아래, 이 여정 중에 얻은 수많은 '흉터'들 속에 숨어 있다. 개발자의 성장은 깨끗하고 우상향하는 주식 차트가 아니라, 행복한 사고와 생산적인 실패, 그리고 잘못된 길에서 돌아 나오며 얻은 값비싼 교훈들의 총합으로 이루어진, 삐뚤빼뚤한 심전도 그래프와 같다는 것을 이제는 안다. 내 성장을 견인했던 흉터들을 하나씩 복기해본다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째 흉터는 &lt;b&gt;자신감 부족이 찍어낸 '전략적 비겁함'&lt;/b&gt;이라는 이름의 아키텍처다. 인프라에 대한 무지로 인해 마이크로서비스라는 '정답'을 외면하고 모놀리식 아키텍처에 기생하기로 한 내 결정은, 순수한 기술적 관점에서는 명백한 후퇴였다. 하지만 지금 돌이켜보면 그 선택은 단순히 두려움의 산물이 아니라, '운영 복잡성이라는 비용을 의식적으로 미래로 이연'하는, 지극히 현실적인 비즈니스 트레이드오프였다. 나는 미래의 리팩토링 비용을 담보로 '시장 출시 속도(Time-to-Market)'라는 단기 생존 가능성에 모든 것을 베팅한 셈이다. 이는 기술 부채가 생존에 위협이 되기 전에 제품-시장 적합성을 달성하려는 스타트업의 베팅과도 같았다. 결과적으로 이 결정은 프로젝트를 정해진 시간 안에, 최소한의 자원으로 완수하게 만든 핵심 동력이었다. 이 경험을 통해 나는 아키텍처 결정이 기술적 순수성뿐만 아니라, 팀의 역량, 비즈니스 목표, 그리고 때로는 리드 개발자의 공포심 같은 인간적인 변수들에 의해 좌우되는 복잡한 방정식임을 깨달았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;두 번째 흉터는 &lt;b&gt;소통의 오해가 낳은 '강제 레벨업'&lt;/b&gt;이다. 선배의 소크라테스식 질문을 해고 통지서로 오독하고 벌였던 며칠간의 밤샘 리팩토링은, 돌이켜보면 압축된 아키텍처 부트캠프였다. 그 고통스러운 과정 속에서 나는 추상적인 개념들을 피와 살이 있는 경험으로 체득했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선배가 물었던 &quot;서비스의 &lt;b&gt;책임 경계&lt;/b&gt;&quot;는 &lt;b&gt;도메인 주도 설계(DDD)&lt;/b&gt;의 '바운디드 컨텍스트'에 대한 화두였지만, 나는 이를 '데이터 소유권 개념도 없는 놈'이라는 질책으로 받아들였다. 나는 물류센터 메타 정보를 우리 서비스 DB에서 삭제하고 기존 '물류 서비스'에 API를 요청해 받아오도록 변경했는데, 이는 &lt;b&gt;'단일 진실 공급원(SSOT)'&lt;/b&gt; 원칙을 지키려는 처절한 몸부림이었지만, 불필요한 네트워크 호출을 폭증시켜 성능을 저해하는 결과를 낳았다.&lt;/li&gt;
&lt;li&gt;&quot;&lt;b&gt;데이터 접근 전략&lt;/b&gt;의 트레이드오프&quot;에 대한 고민 제안은 시스템의 &lt;b&gt;복원력(Resilience)&lt;/b&gt;을 위한 캐싱 전략에 대한 힌트였다. 나는 이를 '자네 코드는 시한폭탄'이라는 경고로 해석하고, 급하게 Redis에 &lt;code&gt;Cache-Aside&lt;/code&gt; 패턴을 구현했다. 하지만 캐시 동기화 문제를 깊이 고민하지 않은 탓에 '오래된 데이터(Stale Data)'를 서빙할 수 있는 또 다른 시한폭탄을 만들고 말았다.&lt;/li&gt;
&lt;li&gt;&quot;미래의 기능 확장을 고려할 때 &lt;b&gt;단일 서비스 구조&lt;/b&gt;가 유효할까?&quot;라는 질문은 &lt;b&gt;단일 책임 원칙(SRP)&lt;/b&gt;에 대한 성찰의 기회였다. 하지만 내게는 '한 치 앞도 못 보는군'이라는 꾸지람으로 들렸고, 나는 아직 존재하지도 않는 기능들을 위한 인터페이스와 추상 클래스를 미리 설계하는 명백한 &lt;b&gt;'과잉 설계(Over-engineering)'&lt;/b&gt;의 늪에 빠졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 고통스러운 삽질은 내가 단순히 '동작하는 코드'를 넘어, 시스템 설계의 근본적인 '왜'를 고민하도록 강제했다. 이 단어들은 더 이상 책 속에 박제된 개념이 아니었다. 그것은 내가 밤을 새워가며 코드를 지우고 다시 쓰기를 반복했던 처절한 사투의 흔적이자, 고통을 통해 내 머릿속에 각인된 살아있는 지혜가 되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;마지막 흉터는 &lt;b&gt;언어에 대한 불안감이 쏘아 올린 '공감의 기술 문서'&lt;/b&gt;다. 전화벨에 대한 공포는 역설적으로, 국경과 언어를 넘어선 완벽한 소통 도구를 만들겠다는 집착에 가까운 열망을 불태웠다. 스트라이프(Stripe)의 문서를 교본 삼아, 나는 파트너사 개발자의 '경험'을 설계했다. 명확한 요청/응답 JSON 예시는 그 자체로 오해의 소지가 없는 공용어가 되었고, 모든 에러 케이스를 설명하는 '포괄적인 오류 코드 용어집'은 기술의 형태를 띤 공감의 표현이었다. 이 경험을 통해 나는 훌륭한 문서가 단순히 정보를 전달하는 것을 넘어, 팀 간의 신뢰를 구축하고 협업의 마찰 비용을 극적으로 줄이는 핵심적인 전략 자산임을 깨달았다. API 명세서는 팀 간의 공식적인 '기술적 평화 협정'이었던 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나는 더 이상 우연히 프로젝트를 떠맡았던 겁 많은 '설계자'가 아니다. 위기라는 용광로 속에서 단련된, 흉터의 지혜를 가진 엔지니어로 다시 태어났다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;좋은 리더의 현명한 신뢰와, 인내심 많은 멘토의 깊은 지혜, 그리고 일이 잘못되었을 때 배우는 고통스러운 가르침에 깊은 감사를 전한다. 진정한 엔지니어링의 탁월함은 실수를 피하는 데 있는 것이 아니라, 그 실수가 남긴 흉터를 나침반 삼아 더 나은 것을 만들어나가는 능력에 있음을 배웠기 때문이다.&lt;/p&gt;</description>
      <category>에세이</category>
      <author>북항</author>
      <guid isPermaLink="true">https://webfirewood.tistory.com/144</guid>
      <comments>https://webfirewood.tistory.com/144#entry144comment</comments>
      <pubDate>Sun, 11 Jul 2021 23:45:00 +0900</pubDate>
    </item>
    <item>
      <title>[Gradle, JAVA, SPRING] 웹개발에 필요한 최소한의 Gradle</title>
      <link>https://webfirewood.tistory.com/129</link>
      <description>&lt;p style=&quot;text-align: left;&quot;&gt;Gradle은 JAVA 개발에 엄청난 도움을 주는 강력한 Build Tool이지만 생소한 groovy 문법과 설정 파일, 사용방법 때문에 오히려 신입 개발자 들을 헷갈리게 하기도 합니다. 다행인 점은 gradle 을 사용하기 위한 학습 곡선이 높지 않고 대부분의 팀에서 gradle을 어렵게 사용하지 않는다는 점입니다. 이 포스트에서는 웹 개발에 필요한 최소한의 gradle 지식을 알아 보도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Build Tool&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;Gradle 은 일종의 build tool(빌드 도구) 입니다. 여기서 말하는 빌드라는 개념은 단순히 프로그램을 컴파일하여 애플리케이션을 생성하는 작업만을 의미하지는 않습니다. 개발한 소프트웨어가 제품으로 만들어지는 일련의 과정, 즉, 컴파일, 테스트, 배포, 문서화 등의 작업을 포함하는 절차를 의미합니다. 이 때, 빌드의 모든 과정을 자동으로 처리할 수 있도록 도와주는 것을 빌드 도구(build tool) 이라고 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;빌드 도구로 불리는 프로그램은 많습니다. 자바에서 이용하는 주요 빌드 도구의 종류도 몇가지나 됩니다. 그 중에서도 최근 대세라고 할 수 있는 것이 Gradle 입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Groovy, DSL(Domain-Specific Languages, 도메인 특화 언어)&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;Gradle은 groovy라고 하는 JVM기반의 동적 타이핑 언어를 사용해서 기술됩니다. groovy는 대부분의 자바 개발자들이 쉽게 배워서 사용할 수 있는 장점이 있습니다. 그런데 gradle은 groovy문법 그 자체를 그대로 사용하지는 않고 &lt;span&gt;'&lt;/span&gt;&lt;span&gt;그루비&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;기반의&lt;/span&gt;&lt;span&gt; DSL(&lt;/span&gt;Domain-Specific Languages)&lt;span&gt;'을 사용합니다. &lt;/span&gt;&lt;span&gt;DSL&lt;/span&gt;&lt;span&gt;은&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;도메인&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;고유&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;언어라고&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;불리는데&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;특정한&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;용도에&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;한정된&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;언어를&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;가리킵니다&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;기반이&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;언어가&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;있지만&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;그&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;언어&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;그&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;자체는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;아니고&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;특정한&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;용도에&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;맞게&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;해당&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;언어를&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;기반으로&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;각색한&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;것입니다&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;그레이들에&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;사용되는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;언어는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;그루비를&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;기반으로&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;작성된&lt;/span&gt;&lt;span&gt; gradle dsl&lt;/span&gt;&lt;span&gt;입니다&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;Gradle 에 대해서 어렵게 생각할 필요는 전혀 없습니다. 애초에 개발을 더 쉽게 할 수 있도록 도움을 주기 위해 탄생한 빌드 도구인 만큼 학습에 많은 시간을 필요로 하지 않기 때문입니다. 실제 웹 프로젝트에서 사용하는 기술을 바탕으로 gradle을 조금 더 알아 보도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;build.gradle&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;build.gradle 은 gradle에서 빌드 작업에 필요한 기본 설정, 동작 등을 정의하는 파일입니다. 아래는 IDEA(intellij) 에서 Spring-boot 프로젝트를 생성하고 몇 가지 dependency를 추가했을 때 기본적으로 생성해 주는 build.gradle 파일입니다. 그냥 딱 봐도 알 수 있는 부분은 넘어가기로 하고 필요한 부분들에 대해 간단히 설명해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1595056489021&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    plugins {
        id 'org.springframework.boot' version '2.3.1.RELEASE'
        id 'io.spring.dependency-management' version '1.0.9.RELEASE'
        id 'java'
    }
    
    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '1.8'

    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
        implementation 'org.springframework.boot:spring-boot-starter-security'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.kafka:spring-kafka'
        compileOnly 'org.projectlombok:lombok'
        runtimeOnly 'com.h2database:h2'
        runtimeOnly 'mysql:mysql-connector-java'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation('org.springframework.boot:spring-boot-starter-test') {
            exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
        }
        testImplementation 'org.springframework.kafka:spring-kafka-test'
        testImplementation 'org.springframework.security:spring-security-test'
    }

    test {
        useJUnitPlatform()
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;plugins&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;프로젝트를 빌드하기 위해서 여러가지 작업을 처리해 줘야 합니다. 컴파일이나 jar 파일의 생성 같은 작업들이죠. 이런 작업들을 해주는 플러그인들이 존재합니다. plugins 블록 안에 필요한 플러그인을 지정해주고 이런 플로그인들은 필요한 과정들을 task로 포함하고 있습니다. 빌드시에는 필요한 모든 과정을 플러그인의 내부 task가 진행해 주게 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;repositories&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;repositories&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;는&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;저장소&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;정보를&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;관리하는&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;프로퍼티입니다&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;소프트웨어를&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;등록하여&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;관리하는&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;장소를&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;가리킵니다&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;로컬&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;환경이나&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;네트워크에&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;라이브러리를&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;공개하고&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;주소를&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;저장소로&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;등록하면&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;저장소에&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;있는&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;라이브러리를&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그레이들이&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;취득하여&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이용할 수 있습니다. 위에 기술된 repositories에는 mavenCentral() 이라는 메소드를 통해 중앙저장소를 사용하고 있습니다. jCenter() 메서드를 사용해 jCenter 저장소를 이용할 수도 있습니다. jCenter는 그레이들에서 중앙 저장소로 이용하는 저장소 입니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;저장소는 각종 라이브러리 등이 등록된 일종이 소프트웨어 보관 장소라고 할 수 있습니다. 저장소에는 각종 프로그램이 등록되어 있어 그레이들은 저장소로부터 필요 따라 프로그램을 다운로드 하여 이용할 수 있습니다. 또 다른 빌드 도구중 하나인 메이븐은 메이븐 중앙 저장소를 제공하며 그레이들에서도 이 메이븐 중앙 저장소에 접속해서 필요한 프로그램을 다운로드 할 수 있습니다. 이 중앙 저장소는 현재 아파치 재단에서 운영 중입니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;만약 중앙저장소에 공개되지 않았거나 공개하기 힘든 라이브러리가 있을 수도 있습니다. 예를 들어 사내에서만 사용하는 라이브러리라면 중앙 저장소에 공개하기는 힘들것입니다. 이런 경우는 로컬 저장소를 이용하면 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;dependencies&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&lt;span&gt;Dependencies &lt;/span&gt;&lt;span&gt;는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;의존성에&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;관한&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;설정을&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;관리하는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;프로퍼티입니다&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;여기에&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;필요한&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;라이브러리&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;등의&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;정보를&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;기술하면&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;그&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;라이브러리를&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;참조할&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;수&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;있습니다&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&lt;span&gt;위 예시 파일에서는 implementation, testImplementation &lt;/span&gt;&lt;span&gt;이라고&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;기술되어&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;있습니다&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;컴파일&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;할&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;때는&lt;/span&gt;&lt;span&gt; &lt;span style=&quot;color: #333333;&quot;&gt;implementation&lt;/span&gt;&lt;/span&gt;&lt;span&gt;에&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;지정한&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;라이브러리에&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;그리고&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;테스트를&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;컴파일할&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;때는&lt;/span&gt;&lt;span&gt; &lt;span style=&quot;color: #333333;&quot;&gt;testImplementation&lt;/span&gt;&lt;/span&gt;&lt;span&gt;에&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;지정한&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;라이브러리에&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;각각&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;접근할&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;수&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;있다는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;의미입니다&lt;/span&gt;&lt;span&gt;. 이외에 compileOnly는 컴파일시에만, runtimeOnly는 런타임시에만 사용한다는 의미입니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1595058778870&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    compile group: 'org.hibernate', name: 'hibernate-core', version: '3.6.7.Final'
    // 짧게 쓰면 &quot;group:name:version&quot;
    compile 'org.hibernate:hibernate-core:3.6.7.Final'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;dependenc를 추가하는 방법에는 위와같이 두 가지가 존재하는데, 후자는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;축약형인데&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;의미의&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;차이는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;없으므로&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;자신이&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;선호하는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;방법으로 기&lt;/span&gt;&lt;span&gt;술하면 됩니다.&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;어느쪽을&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;이용하더라도&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;전혀&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;문제가&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;되지&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;않습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;span&gt;ext&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;위 예시에서는 등장하지 않지만&lt;span style=&quot;color: #333333;&quot;&gt;(아래 멀티모듈인 경우 사용 예시를 참고해주세요)&lt;/span&gt; ext 블록이 사용된 경우가 있습니다. 이 블록은 gradle 의 모든 task 에서 사용할 수 있는 일조의 전역 변수를 선언하는 블록이라고 생각하면 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;buildscript&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;역시 위 예시에서는 등장하지 않지만(아래 멀티모듈인 경우 사용 예시를 참고해주세요) buildscript 블록을 사용할 때가 있습니다. buildscript는 &lt;span&gt;빌드하는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;동안&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;필요한&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;처리를&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;모아놓는&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;곳입니다&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;이&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;안에서&lt;/span&gt;&lt;span&gt; dependencies&lt;/span&gt;&lt;span&gt;와&lt;/span&gt;&lt;span&gt; repositories&lt;/span&gt;&lt;span&gt;가&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;포함 할 수 있습니다&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;settings.gradle&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;또다른 그레이들 파일 중에 settings.gradle 이 있습니다. 여러가지로 사용할 수 있지만 가장 많이 사용하는 것은 역시 멀티모듈을 사용할 때 설정입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1595059060243&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rootProject.name = 'example'
include 'sub-example'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;위 예시에서 rootProject.name 을 통해 루트프로젝트를 설정하고 include 를 통해 하위 모듈을 설정했습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;멀티모듈일 때 build.gradle&lt;/h3&gt;
&lt;pre id=&quot;code_1595059384644&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;buildscript {
    ext {
        springBootVersion = '2.1.3.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath(&quot;org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}&quot;)
        classpath &quot;io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE&quot;
    }
}

allprojects {
    group 'com.example'
    version '1.0-SNAPSHOT'
}

subprojects {
    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    sourceCompatibility = 1.8

    repositories {
        mavenCentral()
    }

    dependencies {
        testCompile group: 'junit', name: 'junit', version: '4.12'
    }
}

project(':example') {
    dependencies {
        ....
    }
}

project(':sub-example') {
    dependencies {
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;allprojects, subprojcets, project&lt;/h3&gt;
&lt;p&gt;멀티 모듈인 경우 이처럼 새로운 블록이 등장합니다. 상당히 직관적이지만 굳이 설명 하자면 allprojects는 전체 프로젝트에, subprojects는 하위 프로젝트에, 그리고 프로젝트 이름을 사용한 project 는 해당하는 프로젝트에만 동작하는 설정입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;task&lt;/h3&gt;
&lt;p&gt;마지막으로 사용자가 임의로 task를 작성해서 사용할 수 있습니다. task는 다양한 기능을 수행할 수 있고 다양한 문법을 가지고 있지만 아마 신입 개발자가 실무에서 작성하는 경우는 극히 제한적일 것입니다. 깊이 있는 내용을 담기에는 포스팅의 성격을 벗어나기 때문에 간단히 언급하고 넘어가겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1595060064507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;task exampleTest {
    println 'Hello World!'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;task 는 위 예시처럼 선언할 수 있으며 커맨들 라인에서 task [task 이름]으로 사용할 수 있습니다. 예시를 실행하면 다음과 같은 결과를 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sRXc1/btqFOg9GPhr/a9YpYqQEmFl1NYSe7l0UMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sRXc1/btqFOg9GPhr/a9YpYqQEmFl1NYSe7l0UMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sRXc1/btqFOg9GPhr/a9YpYqQEmFl1NYSe7l0UMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsRXc1%2FbtqFOg9GPhr%2Fa9YpYqQEmFl1NYSe7l0UMK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이상으로 웹 개발에 필요한 최소한의 gradle 지식을 살펴 보았습니다.&amp;nbsp; 부족한 부분이 많을 수도 있지만 신입 개발자가 팀에 합류한 어려움을 어느정도는 해소해 줄 수 있는 수준은 된다고 생각합니다. 갑사합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr style=&quot;margin: 20px auto 0px; border: none; cursor: pointer !important; z-index: 1; font-size: 0px; line-height: 0; background: url('../image/divider-line.svg') center -256px / 200px 420px repeat-x; height: 2px; padding: 21px 0px; color: #333333; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고서적&lt;/p&gt;
&lt;figure id=&quot;og_1595060393729&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;엔터프라이즈 빌드 자동화를 위한 Gradle&quot; data-og-description=&quot;『엔터프라이즈 빌드 자동화를 위한 Gradle』은 자바 웹 개발자를 대상으로 빌드에 대한 체계적인 이론을 학습할 수 있게 하면서 BDD, CI, CD, 통합테스트를 실천하는 데 필요한 도구들(Spock, Jenkins, G&quot; data-og-host=&quot;www.hanbit.co.kr&quot; data-og-source-url=&quot;https://www.hanbit.co.kr/realtime/books/book_view.html?p_code=E1458217888&quot; data-og-url=&quot;https://www.hanbit.co.kr/realtime/books/book_view.html?p_code=E1458217888&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/be9lvq/hyGPhkHXWm/srP8aQxJBQDzU7ebmlDxQk/img.jpg?width=183&amp;amp;height=260&amp;amp;face=0_0_183_260,https://scrap.kakaocdn.net/dn/5QfJR/hyGPearKCR/xtqr6s0fBHmhKIUCWiWlzK/img.jpg?width=400&amp;amp;height=568&amp;amp;face=0_0_400_568,https://scrap.kakaocdn.net/dn/ksj2V/hyGNORZZUI/LkPkz8ZtuhhhUuQL62G9I1/img.jpg?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200&quot;&gt;&lt;a href=&quot;https://www.hanbit.co.kr/realtime/books/book_view.html?p_code=E1458217888&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.hanbit.co.kr/realtime/books/book_view.html?p_code=E1458217888&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/be9lvq/hyGPhkHXWm/srP8aQxJBQDzU7ebmlDxQk/img.jpg?width=183&amp;amp;height=260&amp;amp;face=0_0_183_260,https://scrap.kakaocdn.net/dn/5QfJR/hyGPearKCR/xtqr6s0fBHmhKIUCWiWlzK/img.jpg?width=400&amp;amp;height=568&amp;amp;face=0_0_400_568,https://scrap.kakaocdn.net/dn/ksj2V/hyGNORZZUI/LkPkz8ZtuhhhUuQL62G9I1/img.jpg?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;엔터프라이즈 빌드 자동화를 위한 Gradle&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;『엔터프라이즈 빌드 자동화를 위한 Gradle』은 자바 웹 개발자를 대상으로 빌드에 대한 체계적인 이론을 학습할 수 있게 하면서 BDD, CI, CD, 통합테스트를 실천하는 데 필요한 도구들(Spock, Jenkins, G&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;www.hanbit.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1595060419788&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;자바 프로젝트 필수 유틸리티&quot; data-og-description=&quot;이 책은 자바 프로젝트를 수행하는 데 유용한 깃/깃허브, 젠킨스, 메이븐, 그레이들, SBT를 협업 관점에서 소개합니다. 단순히 기능만 소개하는 것이 아니라 현업에 유용한 플러그인들을 활용하��&quot; data-og-host=&quot;www.hanbit.co.kr&quot; data-og-source-url=&quot;https://www.hanbit.co.kr/store/books/look.php?p_code=B8716214772&quot; data-og-url=&quot;https://www.hanbit.co.kr/store/books/look.php?p_code=B8716214772&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dAAtGo/hyGNOLewyC/HEZM1bmJ7jgbDmptxO9AG0/img.jpg?width=202&amp;amp;height=260&amp;amp;face=0_0_202_260,https://scrap.kakaocdn.net/dn/R93vC/hyGNRgQUr8/v2CeRgtdGE266SjnFKYXv0/img.jpg?width=1280&amp;amp;height=960&amp;amp;face=0_0_1280_960,https://scrap.kakaocdn.net/dn/y6MKG/hyGPdbxd3C/yp06ZAvZII8DV9kDKzHwq1/img.jpg?width=800&amp;amp;height=1066&amp;amp;face=0_0_800_1066&quot;&gt;&lt;a href=&quot;https://www.hanbit.co.kr/store/books/look.php?p_code=B8716214772&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.hanbit.co.kr/store/books/look.php?p_code=B8716214772&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dAAtGo/hyGNOLewyC/HEZM1bmJ7jgbDmptxO9AG0/img.jpg?width=202&amp;amp;height=260&amp;amp;face=0_0_202_260,https://scrap.kakaocdn.net/dn/R93vC/hyGNRgQUr8/v2CeRgtdGE266SjnFKYXv0/img.jpg?width=1280&amp;amp;height=960&amp;amp;face=0_0_1280_960,https://scrap.kakaocdn.net/dn/y6MKG/hyGPdbxd3C/yp06ZAvZII8DV9kDKzHwq1/img.jpg?width=800&amp;amp;height=1066&amp;amp;face=0_0_800_1066');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;자바 프로젝트 필수 유틸리티&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;이 책은 자바 프로젝트를 수행하는 데 유용한 깃/깃허브, 젠킨스, 메이븐, 그레이들, SBT를 협업 관점에서 소개합니다. 단순히 기능만 소개하는 것이 아니라 현업에 유용한 플러그인들을 활용하��&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;www.hanbit.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>framework/Spring</category>
      <author>북항</author>
      <guid isPermaLink="true">https://webfirewood.tistory.com/129</guid>
      <comments>https://webfirewood.tistory.com/129#entry129comment</comments>
      <pubDate>Sat, 18 Jul 2020 17:20:38 +0900</pubDate>
    </item>
    <item>
      <title>SPRING SECURITY + JWT 회원가입, 로그인 기능 구현</title>
      <link>https://webfirewood.tistory.com/115</link>
      <description>&lt;p&gt;이전에 서블릿 보안과 관련된 포스트(&lt;a href=&quot;https://webfirewood.tistory.com/57&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;)를 작성했던 적이 있습니다. 서블릿 기반의 웹 애플리케이션에서 인증과 인가 과정을 간단하게 설명했습니다. 스프링에서는 마찬가지로 이런 인증과 권한등 보안에 관한 기능을 제공하는 프레임워크인 스프링 시큐리티(Spring Security)가 있습니다. 스프링 시큐리티는 보안과 관련되어 수행해야 하는 다양한 작업들을 지원해 줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 저와 같은 신입 개발자들이 스프링 시큐리티와 같은 보안기술을 이해하는 것은 쉬운 일이 아닌 것 같습니다. 여전히 모르는 것이 더 많고 어렵게 느껴지는 부분이지만, 최근 진행한 팀 프로젝트에서 스프링 시큐리티와 관련된 기술을 쓰며 알게된 사실들과 코드를 포스팅해 보려고 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인증(Authentication)과 권한(Authorization)&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;먼저 스프링 시큐리티에서 애플리케이션 보안을 구성하는 두 가지 영역에 대해 간단하게 설명해 보도록 하겠습니다. 이 두 영역은 사실상 스프링 시큐리티의 핵심이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;a href=&quot;https://dev.to/caffiendkitten/authentication-vs-authorization-25lc&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5qG8P/btqAVGG9EYf/IEvuX8j5KKbXVmQT8vEefK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5qG8P%2FbtqAVGG9EYf%2FIEvuX8j5KKbXVmQT8vEefK%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/a&gt;&lt;figcaption&gt;https://dev.to/caffiendkitten/authentication-vs-authorization-25lc&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;인증(Authentication)은 보호된 리소스에 접근하는 대상, 즉 사용자에게 적절한 접근 권한이 있는지 확인하는 일련의 과정을 의미합니다. 이 때 보호된 리소스에 접근하는 대상(사용자)을 접근 주체(Principal)이라고 합니다. 권한(Authorization)은 인증절차가 끝난 접근 주체가 보호된 리소스에 접근 가능한지를 결정하는 것을 의미합니다. 이 때 권한을 부여하는 작업을 인가(Authorize)라고 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;쉽게 말하면 인증은 아이디와 비밀번호를 입력 받아 로그인 하는 과정 자체를 의미하는 것이고 권한이 필요한 리소스에 접근하기 위해서는 당연히 이러한 인증 과정을 거쳐야 합니다. 스프링 시큐리는 이런 인증 매커니즘을 간단하게 만들 수 있도록 다양한 옵션들을 제공하고 있습니다. 또한 스프링 시큐리티는 웹 요청이나 메소드 호출, 도메인 인스턴스에 대한 접근 등 상당히 깊은 수준의 권한 부여를 제공하고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스프링 시큐리티의 구조&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;스프링 시큐리티는 주로 서블릿 필터와 이들로 구성된 필터체인을 사용하고 있습니다. 서블릿 필터와 관련된 설명은 이전 포스팅(&lt;a href=&quot;https://webfirewood.tistory.com/58&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;)을 참조 부탁 드립니다. 그렇다면 실제 로그인 시에 스프링 시큐리티의 동작 플로우를 바탕으로 인증과 관련된 스프링 시큐리티의 아키텍쳐를 알아 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAp74D/btqAWyBRZsE/Lk6EL0R680ykd45G6A5rK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAp74D/btqAWyBRZsE/Lk6EL0R680ykd45G6A5rK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAp74D/btqAWyBRZsE/Lk6EL0R680ykd45G6A5rK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAp74D%2FbtqAWyBRZsE%2FLk6EL0R680ykd45G6A5rK1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 그림의 동작 플로우를 간단히 설명하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. 사용자가 로그인 정보와 함께 인증 요청(Http Request)&lt;/p&gt;
&lt;p&gt;2. AuthenticationFilter가 이 요청을 가로챕니다. 이 때 가로챈 정보를 통해 UsernamePasswordAuthenticationToken이라는 인증용 객체를 생성합니다.&lt;/p&gt;
&lt;p&gt;3. &lt;span style=&quot;color: #333333;&quot;&gt;AuthenticationManager&lt;/span&gt;의 구현체인 &lt;span style=&quot;color: #333333;&quot;&gt;ProviderManager&lt;/span&gt;에게 UsernamePasswordAuthenticationToken 객체를 전달합니다.&lt;/p&gt;
&lt;p&gt;4. 다시 AuthenticationProvider에 UsernamePasswordAuthenticationToken 객체를 전달합니다.&lt;/p&gt;
&lt;p&gt;5. 실제 데이터베이스에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보(아이디)를 넘겨줍니다.&lt;/p&gt;
&lt;p&gt;6. 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만듭니다. 이 때 UserDetails 는 인증용 객체와 도메인용 객체를 분리하지 않고 인증용 객체에 상속해서 사용하기도 합니다.&lt;/p&gt;
&lt;p&gt;7. AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보를 비교합니다.&lt;/p&gt;
&lt;p&gt;8. 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환합니다.&lt;/p&gt;
&lt;p&gt;9. 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환됩니다.&lt;/p&gt;
&lt;p&gt;10. Authentication 객체를 SecurityContext에 저장합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;최종적으로 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 &lt;span style=&quot;color: #333333;&quot;&gt;Authentication 객체를 저장합니다. 세션에 사용자정보를 저장한다는 것은 스프링 시큐리티가 전통적인 세션-쿠키 기반의 인증 방식을 사용한다는 것을 의미합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring Security Filter&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;위의 흐름도를 통해 개략적인 인증 과정의 흐름을 알게 되었습니다. 하지만 실제로 스프링 시큐리티는 훨씬 다양한 필터체인을 사용하여 다양한 커스터마이징을 할 수 있도록 돕습니다. 모든 필터를 다 외우고 있을 필요까지는 없겠지만 대략적인 내용을 이해하고 있으면 사용할 때 훨씬 쉽게 관련된 정보를 찾아볼 수 있을 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nc2vV/btqA0OKrJuA/EJowMYkCIOIuS6bx09Y1T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nc2vV/btqA0OKrJuA/EJowMYkCIOIuS6bx09Y1T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nc2vV/btqA0OKrJuA/EJowMYkCIOIuS6bx09Y1T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnc2vV%2FbtqA0OKrJuA%2FEJowMYkCIOIuS6bx09Y1T1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: square;&quot; data-ke-list-type=&quot;square&quot;&gt;
&lt;li&gt;SecurityContextPersistentFilter : SecurityContextRepository에서 SecurityContext를 가져와서 SecurityContextHolder에 주입하거나 반대로 저장하는 역할을 합니다.&lt;/li&gt;
&lt;li&gt;LogoutFilter : logout 요청을 감시하며, 요청시 인증 주체(Principal)를 로그아웃 시킵니다.&lt;/li&gt;
&lt;li&gt;UsernamePasswordAuthenticationFilter : login 요청을 감시하며, 인증 과정을 진행합니다.&lt;/li&gt;
&lt;li&gt;DefaultLoginPageGenerationFilter : 사용자가 별도의 로그인 페이지를 구현하지 않은 경우, 스프링에서 기본적으로 설정한 로그인 페이지로 넘어가게 합니다.&lt;/li&gt;
&lt;li&gt;BasicAuthenticationFilter : HTTP 요청의 (BASIC)인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장합니다.&lt;/li&gt;
&lt;li&gt;RememberMeAuthenticationFilter : SecurityContext에 인증(Authentication) 객체가 있는지 확인하고 RememberMeServices를 구현한 객체 요청이 있을 경우, RememberMe를 인증 토큰으로 컨텍스트에 주입합니다.&lt;/li&gt;
&lt;li&gt;AnonymousAuthenticationFilter : 이 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면 익명 사용자로 취급합니다.&lt;/li&gt;
&lt;li&gt;SessionManagementFilter : 요청이 시작된 이후 인증된 사용자인지 확인하고, 인증된 사용자일 경우 SessionAuthenticationStrategy를 호출하여 세션 고정 보호 매커니즘을 활성화 하거나 여러 동시 로그인을 확인하는 것과 같은 세션 관련 활동을 수행합니다.&lt;/li&gt;
&lt;li&gt;ExceptionTranslationFilter : 필터체인 내에서 발생되는 모든 예외를 처리합니다.&lt;/li&gt;
&lt;li&gt;FilterSecurityInterceptor : AccessDecisionManager로 권한부여처리를 위임하고 HTTP 리소스의 보안 처리를 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JWT(Json Web Token)&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;JWT(Json Web Token)은 JSON 객체를 통해 안전하게 정보를 전송할 수 있는 웹표준(RFC7519) 입니다. JWT는 '.'을 구분자로 세 부분으로 구분되어 있는 문자열로 이루어져 있습니다. 각각 헤더는 토큰 타입과 해싱 알고리즘을 저장하고, 내용은 실제로 전달할 정보, 서명에는 위변조를 방지하기위한 값이 들어가게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmtrRL/btqAZO41bpf/6bLetr0rhyyjENyROBfAO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmtrRL/btqAZO41bpf/6bLetr0rhyyjENyROBfAO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmtrRL/btqAZO41bpf/6bLetr0rhyyjENyROBfAO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmtrRL%2FbtqAZO41bpf%2F6bLetr0rhyyjENyROBfAO1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;JWT는 JSON 객체를 암호화 하여 만든 문자열 값으로 위, 변조가 어려운 정보라고 할 수 있습니다. 또, 다른 토큰들과 달리 토큰 자체에 데이터를 가지고 있다는 특징이 있습니다. JWT의 이러한 특징 때문에 사용자의 인증 요청시 필요한 정보를 전달하는 객체로 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBZi1o/btqAY7qdGoM/bshk8EU5SY83F3CVWXzJ30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBZi1o/btqAY7qdGoM/bshk8EU5SY83F3CVWXzJ30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBZi1o/btqAY7qdGoM/bshk8EU5SY83F3CVWXzJ30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBZi1o%2FbtqAY7qdGoM%2Fbshk8EU5SY83F3CVWXzJ30%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;API 서버는 로그인 요청이 완료되면 클라이언트에게 회원을 구분할 수 있는 정보를 담은 JWT를 생성하여 전달합니다. 그러면 클라이언트는 이 JWT를 헤더에 담아서 요청을 하게 됩니다. 권한이 필요한 요청이 있을 때 마다 API 서버는 헤더에 담긴 JWT 값을 확인하고 권한이 있는 사용자인지 확인하고 리소스를 제공하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 기존의 세션-쿠키 기반의 로그인이 아니라 JWT같은 토큰 기반의 로그인을 하게 되면 세션이 유지되지 않는 다중 서버 환경에서도 로그인을 유지할 수 있게 되고 한 번의 로그인으로 유저정보를 공유하는 여러 도메인에서 사용할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 때 회원을 구분할 수 있는 정보가 담기는 곳이 바로 JWT의 payload 부분이고 이곳에 담기는 정보의 한 '조각'을 Claim 이라고 합니다. Claim은 name / value 한 쌍으로 이루어져 있으며 당연히 여러개의 Claim들을 넣을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시작하기&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;이제 직접 코드를 작성해 볼 시간이 되었습니다. 우선 Spring boot gradle 프로젝트를 생성하고 필요한 클래스를 생성해 두겠습니다. 필요한 dependencies 는 다음 builde.gradle 파일을 참고하시기 바랍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578586569265&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'org.springframework.boot' version '2.2.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'java'
}

group = 'com.tistory.webfirewood'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.springframework.security:spring-security-test'
}

test {
    useJUnitPlatform()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;User 정보를 담을 Entity 객체와 Repository 가 필요합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578586511224&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

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

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

    @Column(length = 100, nullable = false, unique = true)
    private String email;

    @Column(length = 300, nullable = false)
    private String password;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1578586327299&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tistory.webfirewood.springsecurityjwt.domain.user;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;SpringSecurity를 사용하기 위해서 gradle.build 파일에 spring security dependency를 추가합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578494312033&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-security'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Spring Security를 사용하기 위해서는 Spring Security Filter Chain 을 사용한다는 것을 명시해 줘야 합니다. 이것은 WebSecurityConfigurerAdapter를 상속받은 클래스에 @EnableWebSecurity 어노테이션을 달아주면 해결됩니다. 자세한 것은 코드를 통해 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578578778340&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tistory.webfirewood.springsecurityjwt.config.security;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()
                .and()
                .authorizeRequests()
                .antMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
                .antMatchers(&quot;/user/**&quot;).hasRole(&quot;USER&quot;)
                .antMatchers(&quot;/**&quot;).permitAll();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드에서 WebSecurityConfigurerAdapter를 상속받은 WebSecurityConfig에 @EnableWebSecurity 애너테이션이 붙어 있는 것을 볼 수 있습니다. 그리고 Override 된 confiure 메소드에서 &quot;/admin/**&quot;, &quot;/user/**&quot; 형식의 URL로 들어오는 요청에 대해 인증을 요구하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;인증을 요구하는 경로로 요청이 들어올 경우 아래와 같이 인증 정보를 요구하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8ZPLa/btqA2b0bFrX/cQVpmZ0WPlF1tbQLKKNHfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8ZPLa/btqA2b0bFrX/cQVpmZ0WPlF1tbQLKKNHfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8ZPLa/btqA2b0bFrX/cQVpmZ0WPlF1tbQLKKNHfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8ZPLa%2FbtqA2b0bFrX%2FcQVpmZ0WPlF1tbQLKKNHfk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러나 우리가 만들고자 하는것은 세션-쿠키 기반의 전통적인 로그인 방법이 아니라 JWT 를 이용한 방법입니다. JWT 형식의 토큰을 발행하고 검증하는 모듈이 필요합니다. 다음과 같은 dependency를 추가해 주도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578579346648&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'io.jsonwebtoken:jjwt:0.9.1'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;추가된 라이브러리를 사용해서 JWT를 생성하고 검증하는 컴포넌트를 만들어 보도록 하겠습니다. JWT에는 토큰 만료 시간이나 회원 권한 정보등을 저장할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578580323629&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tistory.webfirewood.springsecurityjwt.config.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.Date;
import java.util.List;

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {

    private String secretKey = &quot;webfirewood&quot;;

    // 토큰 유효시간 30분
    private long tokenValidTime = 30 * 60 * 1000L;
    
    private final UserDetailsService userDetailsService;

    // 객체 초기화, secretKey를 Base64로 인코딩한다.
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // JWT 토큰 생성
    public String createToken(String userPk, List&amp;lt;String&amp;gt; roles) {
        Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
        claims.put(&quot;roles&quot;, roles); // 정보는 key / value 쌍으로 저장된다.
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과 
                                                                // signature 에 들어갈 secret값 세팅
                .compact();
    }

    // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
        return new UsernamePasswordAuthenticationToken(userDetails, &quot;&quot;, userDetails.getAuthorities());
    }

    // 토큰에서 회원 정보 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    // Request의 Header에서 token 값을 가져옵니다. &quot;X-AUTH-TOKEN&quot; : &quot;TOKEN값'
    public String resolveToken(HttpServletRequest request) {
        return request.getHeader(&quot;X-AUTH-TOKEN&quot;);
    }

    // 토큰의 유효성 + 만료일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws&amp;lt;Claims&amp;gt; claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;토큰을 생성하고 검증하는 컴포넌트를 완성했지만 실제로 이 컴포넌트를 이용하는 것은 인증 작업을 진행하는 Filter 입니다. 이 필터는 검증이 끝난 JWT로부터 유저정보를 받아와서 UsernamePasswordAuthenticationFilter 로 전달해야 할 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578581829459&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tistory.webfirewood.springsecurityjwt.config.security;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 헤더에서 JWT 를 받아옵니다.
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
        // 유효한 토큰인지 확인합니다.
        if (token != null &amp;amp;&amp;amp; jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            // SecurityContext 에 Authentication 객체를 저장합니다.
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 다시 SecurityCinfiguration 클래스로 돌아가서 작성한 필터를 등록해 주고 필요한 부분을 채워 넣도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578582985634&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tistory.webfirewood.springsecurityjwt.config.security;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;

    // 암호화에 필요한 PasswordEncoder 를 Bean 등록합니다.
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    // authenticationManager를 Bean 등록합니다.
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다.
                .csrf().disable() // csrf 보안 토큰 disable처리.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다.
                .and()
                .authorizeRequests() // 요청에 대한 사용권한 체크
                .antMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
                .antMatchers(&quot;/user/**&quot;).hasRole(&quot;USER&quot;)
                .anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class);
                // JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 넣는다
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;토큰에 저장된 유저 정보를 활용해야 하기 때문에 CustomUserDetatilService 라는 이름의 클래스를 만들고 UserDetailsService를 상속받아 재정의 하는 과정을 진행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578586097214&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tistory.webfirewood.springsecurityjwt.security.user;

import com.tistory.webfirewood.springsecurityjwt.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class CustomUserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByEmail(username)
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;사용자를 찾을 수 없습니다.&quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드를 보면 UserRepository 에서 Email 을 통해 유저를 찾는 findByEmail 메소드를 사용하는 것을 알 수 있습니다. UserRepository 에 findByEmail 메소드를 추가해 줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578586150821&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tistory.webfirewood.springsecurityjwt.domain.user;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {

    Optional&amp;lt;User&amp;gt; findByEmail(String email);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;SpringSecurity는 UserDetails 객체를 통해 권한 정보를 관리하기 때문에 User 클래스에 UserDetails 를 구현하고 추가 정보를 재정의 해야 합니다. Entity와 UserDetails는 구분할 수도 같은 클래스에서 관리할 수도 있습니다. 여기에서는 같은 클래스에서 관리하는 방법을 사용하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578586810073&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tistory.webfirewood.springsecurityjwt.domain.user;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class User implements UserDetails {

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

    @Column(length = 100, nullable = false, unique = true)
    private String email;

    @Column(length = 30, nullable = false)
    private String password;

    @ElementCollection(fetch = FetchType.EAGER)
    @Builder.Default
    private List&amp;lt;String&amp;gt; roles = new ArrayList&amp;lt;&amp;gt;();

    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        return this.roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;getUsername을 통해 spring security에서 사용하는 username을 가져갑니다. 저희가 사용할 username은 email 입니다. 이제 실제로 Controller 에서 회원 가입과 로그인을 통한 인증 과정을 진행해 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578660736187&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tistory.webfirewood.springsecurityjwt.web.user;

import com.tistory.webfirewood.springsecurityjwt.config.security.JwtTokenProvider;
import com.tistory.webfirewood.springsecurityjwt.domain.user.User;
import com.tistory.webfirewood.springsecurityjwt.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.Map;

@RequiredArgsConstructor
@RestController
public class UserController {

    private final PasswordEncoder passwordEncoder;
    private final JwtTokenProvider jwtTokenProvider;
    private final UserRepository userRepository;

    // 회원가입
    @PostMapping(&quot;/join&quot;)
    public Long join(@RequestBody Map&amp;lt;String, String&amp;gt; user) {
        return userRepository.save(User.builder()
                .email(user.get(&quot;email&quot;))
                .password(passwordEncoder.encode(user.get(&quot;password&quot;)))
                .roles(Collections.singletonList(&quot;ROLE_USER&quot;)) // 최초 가입시 USER 로 설정
                .build()).getId();
    }

    // 로그인
    @PostMapping(&quot;/login&quot;)
    public String login(@RequestBody Map&amp;lt;String, String&amp;gt; user) {
        User member = userRepository.findByEmail(user.get(&quot;email&quot;))
                .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;가입되지 않은 E-MAIL 입니다.&quot;));
        if (!passwordEncoder.matches(user.get(&quot;password&quot;), member.getPassword())) {
            throw new IllegalArgumentException(&quot;잘못된 비밀번호입니다.&quot;);
        }
        return jwtTokenProvider.createToken(member.getUsername(), member.getRoles());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 애플리케이션을 실행하고 POSTMAN등의 도구를 이용해 테스트를 해 봅니다. /join 으로 POST 요청을 보내서 회원가입을 한 뒤, /login 으로 POST 요청을 보내면 토큰을 생성해 반환 하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfKjPM/btqA6Dn13SW/YcsGluG5fFpq3QrNvaskZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfKjPM/btqA6Dn13SW/YcsGluG5fFpq3QrNvaskZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfKjPM/btqA6Dn13SW/YcsGluG5fFpq3QrNvaskZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfKjPM%2FbtqA6Dn13SW%2FYcsGluG5fFpq3QrNvaskZK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 돌려 받은 토큰을 header 에 &quot;X-AUTH-TOKEN&quot; 에 담아 제한된 리소스에 대한 요청을 하게 되면 토큰을 통해 권한을 확인하고 리소스를 반환하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckaMDR/btqA3eC1MHx/F95f88xEFVKzyj9DBEHWy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckaMDR/btqA3eC1MHx/F95f88xEFVKzyj9DBEHWy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckaMDR/btqA3eC1MHx/F95f88xEFVKzyj9DBEHWy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckaMDR%2FbtqA3eC1MHx%2FF95f88xEFVKzyj9DBEHWy1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 Spring Security와 JWT를 이용해서 회원가입과 로그인 기능을 구현해 봤습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고 사이트&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;a href=&quot;https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1578407297518&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;SpringBoot2로 Rest api 만들기(8) &amp;ndash; SpringSecurity 를 이용한 인증 및 권한부여 - 아빠프로그래머의 좌충우돌 개발하기!&quot; data-og-description=&quot;SpringSecurity를 이용하여 api서버의 사용 권한을 제한하는 방법에 대해 알아보도록 하겠습니다. 지금까지 개발한 api는 아무나 접근하여 리소스를 생성, 수정, 삭제가 가능했는데 인증을 통한 회원만 api를 사용할수 있도록 개선해 보도록 하겠습니다.&quot; data-og-host=&quot;daddyprogrammer.org&quot; data-og-source-url=&quot;https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/&quot; data-og-url=&quot;https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/IXozP/hyEuzhRBHv/Mkr5B5wW9eeJbPw3m4BcY1/img.png?width=342&amp;amp;height=414&amp;amp;face=0_0_342_414,https://scrap.kakaocdn.net/dn/Tl2nj/hyEuo1IcjB/xnonieKsmBdBiFQruLqky0/img.png?width=342&amp;amp;height=414&amp;amp;face=0_0_342_414,https://scrap.kakaocdn.net/dn/j9uVC/hyEux5p7dc/f9VBaa4SZnJOTcfwy57KAk/img.jpg?width=1231&amp;amp;height=1066&amp;amp;face=0_0_1231_1066&quot;&gt;&lt;a href=&quot;https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/IXozP/hyEuzhRBHv/Mkr5B5wW9eeJbPw3m4BcY1/img.png?width=342&amp;amp;height=414&amp;amp;face=0_0_342_414,https://scrap.kakaocdn.net/dn/Tl2nj/hyEuo1IcjB/xnonieKsmBdBiFQruLqky0/img.png?width=342&amp;amp;height=414&amp;amp;face=0_0_342_414,https://scrap.kakaocdn.net/dn/j9uVC/hyEux5p7dc/f9VBaa4SZnJOTcfwy57KAk/img.jpg?width=1231&amp;amp;height=1066&amp;amp;face=0_0_1231_1066');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;SpringBoot2로 Rest api 만들기(8) &amp;ndash; SpringSecurity 를 이용한 인증 및 권한부여 - 아빠프로그래머의 좌충우돌 개발하기!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;SpringSecurity를 이용하여 api서버의 사용 권한을 제한하는 방법에 대해 알아보도록 하겠습니다. 지금까지 개발한 api는 아무나 접근하여 리소스를 생성, 수정, 삭제가 가능했는데 인증을 통한 회원만 api를 사용할수 있도록 개선해 보도록 하겠습니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;daddyprogrammer.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://coding-start.tistory.com/153&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://coding-start.tistory.com/153&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1578407318494&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring boot - Spring Security(스프링 시큐리티) 란? 완전 해결!&quot; data-og-description=&quot;오늘 포스팅할 내용은 Spring Security이다. 사실 필자는 머리가 나빠서 그런지 모르겠지만, 아무리 구글링을 통해 스프링 시큐리티를 검색해도 이렇다할 명쾌한 해답을 얻지 못했다. 대부분 이론적인 설명들은..&quot; data-og-host=&quot;coding-start.tistory.com&quot; data-og-source-url=&quot;https://coding-start.tistory.com/153&quot; data-og-url=&quot;https://coding-start.tistory.com/153&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/OlYnE/hyEuxYENX8/6yBPKxA5eZKuDgHkFZ3b7k/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/b4zeWo/hyEuoAEt8K/xjdsXojqTsugjModPmEksK/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/chmva7/hyEutIJHt3/V4b8CVruu3KjjYhLDesPz0/img.png?width=1840&amp;amp;height=1348&amp;amp;face=0_0_1840_1348&quot;&gt;&lt;a href=&quot;https://coding-start.tistory.com/153&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://coding-start.tistory.com/153&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/OlYnE/hyEuxYENX8/6yBPKxA5eZKuDgHkFZ3b7k/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/b4zeWo/hyEuoAEt8K/xjdsXojqTsugjModPmEksK/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/chmva7/hyEutIJHt3/V4b8CVruu3KjjYhLDesPz0/img.png?width=1840&amp;amp;height=1348&amp;amp;face=0_0_1840_1348');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Spring boot - Spring Security(스프링 시큐리티) 란? 완전 해결!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;오늘 포스팅할 내용은 Spring Security이다. 사실 필자는 머리가 나빠서 그런지 모르겠지만, 아무리 구글링을 통해 스프링 시큐리티를 검색해도 이렇다할 명쾌한 해답을 얻지 못했다. 대부분 이론적인 설명들은..&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;coding-start.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://sjh836.tistory.com/165&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://sjh836.tistory.com/165&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1578407346866&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;spring security 파헤치기 (구조, 인증과정, 설정, 핸들러 및 암호화 예제, @Secured, @AuthenticationPrincipal, taglib)&quot; data-og-description=&quot;참조문서 https://docs.spring.io/spring-security/site/docs/4.2.7.RELEASE/reference/htmlsingle/#getting-started http://springsource.tistory.com/80 https://okky.kr/article/382738 1. 스프링 시큐리티란?..&quot; data-og-host=&quot;sjh836.tistory.com&quot; data-og-source-url=&quot;https://sjh836.tistory.com/165&quot; data-og-url=&quot;https://sjh836.tistory.com/165&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/rulbr/hyEun2NF6E/lG3YforRkmU7ESJ9kvVMUk/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/bi6cW0/hyEupzxgIU/9IBThJlzZHrx8DxGwJqgKK/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/bnvbRN/hyEun9ylF1/okC3vNewHGVrHckzf3Mlb0/img.png?width=820&amp;amp;height=615&amp;amp;face=0_0_820_615&quot;&gt;&lt;a href=&quot;https://sjh836.tistory.com/165&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://sjh836.tistory.com/165&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/rulbr/hyEun2NF6E/lG3YforRkmU7ESJ9kvVMUk/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/bi6cW0/hyEupzxgIU/9IBThJlzZHrx8DxGwJqgKK/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/bnvbRN/hyEun9ylF1/okC3vNewHGVrHckzf3Mlb0/img.png?width=820&amp;amp;height=615&amp;amp;face=0_0_820_615');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;spring security 파헤치기 (구조, 인증과정, 설정, 핸들러 및 암호화 예제, @Secured, @AuthenticationPrincipal, taglib)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;참조문서 https://docs.spring.io/spring-security/site/docs/4.2.7.RELEASE/reference/htmlsingle/#getting-started http://springsource.tistory.com/80 https://okky.kr/article/382738 1. 스프링 시큐리티란?..&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;sjh836.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://okky.kr/article/382738&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://okky.kr/article/382738&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1578407367014&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;OKKY | 초보가 이해하는 스프링 시큐리티&quot; data-og-description=&quot;저의 스프링 시큐리티 관련 예제는&amp;nbsp; 깃허브 에서 제공합니다. (주석이 포함된 프로젝트는 주석이 너무 지저분하여 제외...) 1. 스프링 시큐리티란 무엇인가? 스프링 시큐리티를 이해하기 위해서 스프링 시큐리티가 무엇인지를 알아야합니다. 스프링 시큐리티 레퍼런스에서는 자바 EE 기반의 엔터프라이즈 소프트웨어 애플리케이션을 위한 포괄적인 보안 서비스들을&quot; data-og-host=&quot;okky.kr&quot; data-og-source-url=&quot;https://okky.kr/article/382738&quot; data-og-url=&quot;https://okky.kr/article/382738&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bJl5bg/hyEuuHDNPT/5kgXnawaLCo4mlHIdCyrO1/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/bdBmPi/hyEutaSMIj/gdCmeXhYF3FRJsxVNzIPb1/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200&quot;&gt;&lt;a href=&quot;https://okky.kr/article/382738&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://okky.kr/article/382738&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bJl5bg/hyEuuHDNPT/5kgXnawaLCo4mlHIdCyrO1/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/bdBmPi/hyEutaSMIj/gdCmeXhYF3FRJsxVNzIPb1/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;OKKY | 초보가 이해하는 스프링 시큐리티&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;저의 스프링 시큐리티 관련 예제는&amp;nbsp; 깃허브 에서 제공합니다. (주석이 포함된 프로젝트는 주석이 너무 지저분하여 제외...) 1. 스프링 시큐리티란 무엇인가? 스프링 시큐리티를 이해하기 위해서 스프링 시큐리티가 무엇인지를 알아야합니다. 스프링 시큐리티 레퍼런스에서는 자바 EE 기반의 엔터프라이즈 소프트웨어 애플리케이션을 위한 포괄적인 보안 서비스들을&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;okky.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://siyoon210.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://siyoon210.tistory.com/32&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1578488250300&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring Security - Filter, FilterChain&quot; data-og-description=&quot;Spring Security (스프링 시큐리티) 스프링 시큐리티를 이용하면 개발시에 필요한 사용자의 인증, 권한, 보안 처리를 간단하지만 강력하게 구현 할 수 있습니다. 일반적인 웹 환경에서 브라우저가 서버에게 요청을..&quot; data-og-host=&quot;siyoon210.tistory.com&quot; data-og-source-url=&quot;https://siyoon210.tistory.com/32&quot; data-og-url=&quot;https://siyoon210.tistory.com/32&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bkJMed/hyEvOrKmkj/nPN1TE5YqaKoHGLOBKaJ80/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/s4WfN/hyEuuBHiuU/85fpptKOSHvWYnPsOUd5gk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bnZZrQ/hyEuvHjHUK/DItXwZfbPHAXaib9qkckyk/img.png?width=700&amp;amp;height=502&amp;amp;face=0_0_700_502&quot;&gt;&lt;a href=&quot;https://siyoon210.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://siyoon210.tistory.com/32&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bkJMed/hyEvOrKmkj/nPN1TE5YqaKoHGLOBKaJ80/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/s4WfN/hyEuuBHiuU/85fpptKOSHvWYnPsOUd5gk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bnZZrQ/hyEuvHjHUK/DItXwZfbPHAXaib9qkckyk/img.png?width=700&amp;amp;height=502&amp;amp;face=0_0_700_502');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Spring Security - Filter, FilterChain&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Spring Security (스프링 시큐리티) 스프링 시큐리티를 이용하면 개발시에 필요한 사용자의 인증, 권한, 보안 처리를 간단하지만 강력하게 구현 할 수 있습니다. 일반적인 웹 환경에서 브라우저가 서버에게 요청을..&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;siyoon210.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://velopert.com/2389&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velopert.com/2389&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1578496059175&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;[JWT] JSON Web Token 소개 및 구조 | VELOPERT.LOG&quot; data-og-description=&quot;지난 포스트에서는 토큰 기반 인증 시스템의 기본적인 개념에 대하여 알아보았습니다. 이 포스트를 읽기 전에, 토큰 기반 인증 시스템에 대해서 잘 모르시는 분들은 지난 포스트를 꼭 읽어주세요. 이번 포스트에서는, 토큰 기반 인증 시스템의 구현체인 JWT (JSON Web Token) 에 대한 소개를 하고, JWT 토큰의 기본 구조를 알아보도록 하겠습니다. JSON Web Token 이 뭘까? 기본 정보 JSON Web Token (JWT) 은 웹표준 (RFC&quot; data-og-host=&quot;velopert.com&quot; data-og-source-url=&quot;https://velopert.com/2389&quot; data-og-url=&quot;https://velopert.com/2389&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bL3DbK/hyEunP8Mfo/lX6xRqr4bbXsZore7lCtj1/img.png?width=900&amp;amp;height=300&amp;amp;face=0_0_900_300,https://scrap.kakaocdn.net/dn/ce21EE/hyEupmUG7t/xDCl71bq1CDGaINdYr6kRk/img.png?width=900&amp;amp;height=300&amp;amp;face=0_0_900_300,https://scrap.kakaocdn.net/dn/YAGWC/hyEump9sR0/0kWPVznW1GVdlIKnJMVdN0/img.png?width=1148&amp;amp;height=927&amp;amp;face=0_0_1148_927&quot;&gt;&lt;a href=&quot;https://velopert.com/2389&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velopert.com/2389&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bL3DbK/hyEunP8Mfo/lX6xRqr4bbXsZore7lCtj1/img.png?width=900&amp;amp;height=300&amp;amp;face=0_0_900_300,https://scrap.kakaocdn.net/dn/ce21EE/hyEupmUG7t/xDCl71bq1CDGaINdYr6kRk/img.png?width=900&amp;amp;height=300&amp;amp;face=0_0_900_300,https://scrap.kakaocdn.net/dn/YAGWC/hyEump9sR0/0kWPVznW1GVdlIKnJMVdN0/img.png?width=1148&amp;amp;height=927&amp;amp;face=0_0_1148_927');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;[JWT] JSON Web Token 소개 및 구조 | VELOPERT.LOG&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;지난 포스트에서는 토큰 기반 인증 시스템의 기본적인 개념에 대하여 알아보았습니다. 이 포스트를 읽기 전에, 토큰 기반 인증 시스템에 대해서 잘 모르시는 분들은 지난 포스트를 꼭 읽어주세요. 이번 포스트에서는, 토큰 기반 인증 시스템의 구현체인 JWT (JSON Web Token) 에 대한 소개를 하고, JWT 토큰의 기본 구조를 알아보도록 하겠습니다. JSON Web Token 이 뭘까? 기본 정보 JSON Web Token (JWT) 은 웹표준 (RFC&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;velopert.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>framework/Spring</category>
      <author>북항</author>
      <guid isPermaLink="true">https://webfirewood.tistory.com/115</guid>
      <comments>https://webfirewood.tistory.com/115#entry115comment</comments>
      <pubDate>Fri, 10 Jan 2020 23:38:43 +0900</pubDate>
    </item>
    <item>
      <title>NAT(Network Address Translation) &amp;amp; Port-Forwarding</title>
      <link>https://webfirewood.tistory.com/114</link>
      <description>&lt;p&gt;IPv4 는 32비트로 구성된 프로토콜입니다. 32비트로 표현할 수 있는 IP 주소는 대략 42억개 정도로 이미 공인 IP는 2011년 부터 고갈되어 아주 제한적으로 할당이 이루어지고 있습니다.&amp;nbsp;그러나 인터넷이 연결된 개인용 컴퓨터와 모바일 디바이스는 끊임없이 늘어나고 있습니다. 당연히 존재하는 공인 IP만으로 이 모든 장치들을 커버하는 것은 불가능 한 일입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사설 IP&lt;/h4&gt;
&lt;p&gt;사설IP는 부족한 공인 IP 숫자를 보완하기 위한 하나의 수단이 될 수 있습니다. 인터넷 서비스 제공자(ISP, Internet Service Provider)가 부여하는 공인 IP는 외부 인터넷망과 연결된 라우터(공유기)에 부여되고 라우터(공유기)에 연결된 내부 네트워크의 디바이스들은 내부 네트워크에서만 사용되는 가상의 IP주소를 받게 됩니다. 이 가상의 IP 주소가 바로 사설 IP 입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사설IP를 사용하게 되면 공인IP를 하나에 수십, 수백개의 디바이스를 연결해 사용할 수 있습니다. 이 때 외부 인터넷에 접속 하기 위해서 각각의 디바이스는 라우터(공유기)를 통과해야만 하며, 그렇기 때문에 외부에서는 라우터(공유기)에 부여된 공인IP 주소만 식별가능하고 내부 네트워크 디바이스의 사설IP 주소는 알 수 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 했을 때 내부 네트워크에 연결된 디바이스가 라우터를 통해 외부로 요청을 전달할 때는 큰 문제가 없겠지만 반대로 외부 인터넷망에서 응답을 받을 때는 문제가 생깁니다. 외부에서 보여지는 IP 주소는 라우터(공유기)가 할당받은 공인 IP 이기 때문에 외부에서 들어오는 응답 역시 라우터(공유기)를 향하게 됩니다. 그러나 응답을 받은 라우터(공유기)는 해당하는 응답이 어떤 디바이스에 전달되어야 하는지 알 수 없기 때문에 이 응답은 유실되게 되고 결국 디바이스는 계속해서 응답을 받을 수 없게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이런 문제를 해결하기 위해서 필요한 것이 바로 NAT(Network Address Translation)입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;NAT(Network Address Translation)&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;NAT(Network Address Translation)은 이름 그대로 네트워크 주소를 변환해주는 역할을 합니다. NAT는 라우터(공유기) 내부 네트워크에서 외부 인터넷 망으로 요청이 나갈 때 라우터(공유기)에 할당된 공인 IP를 부여해 주고 응답이 전달 될 때 어떤 디바이스의 요청에 대한 응답인지를 알 수 있도록 매핑 데이터를 저장하게 됩니다. 따라서 NAT 는 이 매핑데이터를 저장하는 테이블을 가지고 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjw1by/btqAMln3ct9/BS793g8PWcH8aJrTmYy8tK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjw1by/btqAMln3ct9/BS793g8PWcH8aJrTmYy8tK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjw1by/btqAMln3ct9/BS793g8PWcH8aJrTmYy8tK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjw1by%2FbtqAMln3ct9%2FBS793g8PWcH8aJrTmYy8tK%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;NAT는 요청 헤더에 사설 IP 주소를 저장시키거나, 테이블에 포트 넘버와 같은 매핑 정보를 저장해 두는 등의 방법으로 외부 응답에 대한 디바이스를 구별하게 됩니다. 이 덕분에 내부 네트워크에 연결된 디바이스 들은 사설 IP 를 가지고도 외부 인터넷망에 접근할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그런데 NAT는 내부망에서 외부로 나갈 때 매핑 정보를 저장시켜서 그 응답을 받을 수 있도록 하는 방법입니다. 만약 외부에서 사설 IP로 바로 접근하고자 한다면 어떻게 해야 할까요? 외부에서 보이는 것은 공인 IP 주소 밖에 없기 때문에 외부에서의 요청은 라우터(공유기)에서 사라져 버릴 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Port-fowarding&lt;/h4&gt;
&lt;p&gt;이를 해결하기 위한 방법 중 하나가 바로 포트포워딩(Port-fowarding)입니다. 포트포워딩은 그 이름처럼 요청을 특정한 포트로 포워딩 해 주게 됩니다. 라우터(공유기)의 특정한 포트 번호로 들어오는 요청은 미리 매핑된 내부 네트워크의 디바이스로 포워딩(fowarding)해 주는 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러므로 포트포워딩을 사용하기 위해서는 미리 라우터(공유기)의 특정한 포트 번호와 사설 IP 주소를 매핑해 두어야 합니다. 동적으로 매핑된 정보가 생성되는 NAT와 달리 포트포워딩에서는 매핑 정보가 장기적으로 유지되게 됩니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Network</category>
      <author>북항</author>
      <guid isPermaLink="true">https://webfirewood.tistory.com/114</guid>
      <comments>https://webfirewood.tistory.com/114#entry114comment</comments>
      <pubDate>Fri, 27 Dec 2019 17:15:17 +0900</pubDate>
    </item>
    <item>
      <title>이분탐색(binary search)</title>
      <link>https://webfirewood.tistory.com/108</link>
      <description>&lt;p&gt;알고리즘 책이나 강의를 보면 항상 초반에 한 가지 탐색 방법이 소개 됩니다. 이분탐색(binary search)라는 이름을 가지고 있는 이 알고리즘은 보통 책이나 강좌의 앞쪽에 소개 되는데다 그 개념 자체가 이해하기 어렵지 않기 때문에 배울 때는 큰 어려움이 없지만, 의외로 코딩테스트의 문제로 만나게 되면 문제를 쉽게 풀 수 없게 만드는 복병이 되기도 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;오늘은 이 이분 탐색에 대해 간단히 알아보고 실제 코딩 테스트에서 어떻게 활용할 수 있는지에 대해 생각해 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사실 이분탐색의 개념은 매우 간단하고 직관적입니다. 이를 설명하기 위해 스무고개 놀이를 한 번 해 보도록 하겠습니다. 1부터 100사이에 있는 숫자를 하나 생각하고 가능한 적은 횟수의 추측으로 이 숫자를 알아내는 방법을 생각해 봅시다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;가장 쉬운 방법은 1, 2, 3, 4 순서대로 추측하고 질문하는 것입니다. 만약 '그 숫자가 1인가요?' 라고 물으면 상대방은 '예' 또는 '아니오' 라고 대답하겠죠. '예'라는 답이 나올 때 까지 숫자를 1씩 더해가면서 질문을 이어가면 됩니다. 이런 방법을 단순 탐색(simple search)라고 하는데요. 이 방법으로 질문 하면 한 번 질문할 때 마다 답이 아니라는 사실만을 알게 될 뿐입니다. 만약 답이 22이었다면 정답을 맞추기 전에 스무번의 질문 기회를 모두 다 써버리게 되겠죠.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;이분탐색(binary search)&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;hr&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;당연히 더 좋은 방법이 있습니다. 질문의 방법을 바꾸는 것이죠. 이제는 질문 할 때 마다 그 숫자가 내가 추측한 숫자보다 크거나 같은지를 물어봅니다. 그런데 이렇게 숫자를 1부터 추측해 나가면 모든 숫자는 1보다 크거나 같은 것이 당연하기 떄문에 의미가 없겠죠? 그래서 이번에는 중간 숫자인 50부터 질문을 시작해 나갑니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 '그 숫자가 50보다 큰가요?' 라고 물었을때 대답이 '아니오' 라면 다음 질문은 1부터 49사이의 숫자 중 하나를 골라 질문할 수 있겠죠. 이 때 역시 1부터 49 까지의 숫자 중 중간 숫자에 해당하는 것을 질문하는 것이 가장 효율적일 것입니다. 그러면 다음 질문으로 '그 숫자가 25보다 크거나 같은가요?' 라고 물으면 되겠죠? 이런 질문을 반복해 나가면 금방 정답을 찾을 수 있습니다. 정답이 32라고 정해 놓고 실제로 스무고개를 한 번 해 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;문 : 그 숫자가 50보다 크거나 같은가요?&amp;nbsp; 답 : 아니오&lt;/li&gt;
&lt;li&gt;문 : 그 숫자가 25보다 크거나 같은가요?&amp;nbsp; 답 : 예&lt;/li&gt;
&lt;li&gt;문 : 그 숫자가 37보다 크거나 같은가요?&amp;nbsp; 답 : 아니오&lt;/li&gt;
&lt;li&gt;문 : 그 숫자가 32보다 크거나 같은가요?&amp;nbsp; 답 : 예&lt;/li&gt;
&lt;li&gt;문 : 그 숫자가 34보다 크거나 같은가요?&amp;nbsp; 답 : 아니오&lt;/li&gt;
&lt;li&gt;문 : 그 숫자가 33 보다 크거나 같은가요? 답 : 아니오&lt;/li&gt;
&lt;li&gt;그 숫자는 32 입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;결국 우리는 이 방법을 통해 딱 6번의 질문만에 정답을 찾아 냈습니다. 만약 첫번째 방법으로 32라는 숫자를 찾아내려고 했다면 최소한 32번의 질문이 필요했을 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 무엇이든 절반씩 나누어서 탐색하기 때문에 이분 탐색이라는 이름을 가지게 되었다는 것을 쉽게 유추할 수 있겠죠? 그리고 다들 눈치 채셨겠지만 이 방법은 먼저 '정렬된' 원소들에 대해서만 사용할 수 있습니다. 하지만 정렬된 자료에서는 어떤 경우에도 O(log n) 시간 안에 정답을 찾아낼 수 있죠.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;코드&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;hr&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;이분 탐색을 코드로 구현하면 다음과 같습니다.(JAVA입니다)&lt;/p&gt;
&lt;pre id=&quot;code_1561774681352&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while (start &amp;lt;= end) {
    mid = (start + end) / 2;
    
    ...
    
    if (condition) {
        start = mid + 1;
    } else {
        end = mid - 1
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정렬된 데이터에서 필요한 값을 찾거나, 혹은 필요한 숫자를 찾을 때 이 코드를 사용할 수 있습니다. 개념 자체가 명확하고 직관적인 만큼 코드도 간단합니다. 그러나 우리가 어떤 문제를 풀 때, 이분탐색으로 풀 수 있는 문제인지 무진장 헷갈립니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;중요한 것은 '무엇을 이분탐색으로 찾을 것인가?' 라는 질문에 들어맞는 답이 존재 해야 한다는 것입니다. 말로 하면 어려우니까 실제 예제 문제를 풀어 보면서 설명해 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;프로그래머스 - 입국심사 문제&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;hr&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;예제는 프로그래머스에서 제공하는 입국심사라는 문제를 사용하겠습니다. 문제를 보시려면 이 &lt;a href=&quot;https://programmers.co.kr/learn/courses/30/lessons/43238&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;를 확인해 주시기 바랍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 문제에서 '무엇을 이분탐색을 찾을 것인가?' 라는 질문에 해당하는 답은 무엇일까요? 제 생각에 그 답은 '심사관들에게 주어지는 시간'입니다. 어떤 시간이 주어졌을 때 심사관들이 몇 명을 심사할 수 있는지를 계산할 수 있기 때문에 몇 명을 심사할 수 있는 지를 주어진 n 비교해 n 보다 크면 시간을 줄이고, n보다 작으면 시간을 늘이는 식으로 필요한 시간을 찾아갈 수 있을 것입니다. 이 때 당연히 이 시간을 찾는 방법은 이분탐색이 되겠죠.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;말로 하는 것 보다 코드를 한 번 보는 것이 더 빠를테니 코드를 보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1561775438901&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public long solution(int n, int[] times) {
    	long answer = 0;
        long start = 0;
        long end = Long.MAX_VALUE / 2;
        
        while (start &amp;lt;= end) {
        	long mid = (start + end) / 2;
        	long people = n;
        	for (int time : times) {
    			people -= (long) mid / time;
                if (people &amp;lt; 0) break;
    		}
        	if (people &amp;gt; 0) {
        		start = mid + 1;
        	} else {
        		end = mid - 1;
        		answer = mid;
        	}
        }
        return answer;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아직까지 감이 쉽게 잡히지 않으니 예제를 하나 더 풀어 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;프로그래머스 - 징검다리 문제&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;hr&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;이번 예제 역시 프로그래머스에서 제공하는 징검다리라는 문제를 사용하도록 하겠습니다. 문제를 보시려면 이 &lt;a href=&quot;https://programmers.co.kr/learn/courses/30/lessons/43236&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;로 들어가서 확인해 주세요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에도 역시 '무엇을 이분탐색으로 찾을것인가'의 답을 생각 해야 합니다. 이전 문제에 비해서 그 답을 찾기가 조금 어렵지만 저는 '바위 사이의 최소 거리' 라고 생각했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;바위 사이의 최소거리를 정해놓고 그 최소거리보다 작은 바위들을 다 제거 하는 것이죠. 만약 그 제거하는 바위의 갯수가 주어진 n보다 크다면 거리를 좁혀야 할 것이고, n보다 작다면 거리를 늘여야 합니다. 이 때, 이 최소 거리를 찾는 방법은 당연히 이분탐색이 될 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;백문이불여일코드일테니 코드를 보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1561775814678&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  public int solution(int distance, int[] rocks, int n) {
    
    Arrays.sort(rocks);

    int answer = 1;
    int start = 1;
    int end = distance;
    
    while (start &amp;lt;= end) {
      int mid = (start + end) / 2;
      
      int cnt = 0;
      int last = 0;
      for (int i = 0; i &amp;lt; rocks.length + 1; i++) {
        int gap = i != rocks.length ? rocks[i] - last : distance - rocks[i-1];
        if (gap &amp;lt; mid) {
          cnt++;
        } else if(i != rocks.length) {
          last = rocks[i];
        }
      }
      
      if (cnt &amp;gt; n) {
        end = mid - 1;
      } else {
        start = mid + 1;
        answer = mid;
      }
    }
    
    return answer;
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;거듭 말씀 드리지만 이분탐색은 정렬된 자료에 대해서만 적용가능합니다. 정렬되지 않은 자료에 대해서는 먼저 정렬을 해 주고 사용을 해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자, 이렇게&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;어떤 경우에도&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;O(log n) 시간안에 답을 구할 수 있는 아주 빠른 알고리즘인&lt;/span&gt; 이분탐색의 개념과 실제 코딩테스트에서 나오는 문제 유형을 어떻게 대처하는지 까지 알아 봤습니다. 이분 탐색을 활용해서 풀 수 있는 문제는 아주 많고 다양하지만 오늘은 이 쯤에서 마치도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>algorithm</category>
      <author>북항</author>
      <guid isPermaLink="true">https://webfirewood.tistory.com/108</guid>
      <comments>https://webfirewood.tistory.com/108#entry108comment</comments>
      <pubDate>Sat, 29 Jun 2019 10:43:59 +0900</pubDate>
    </item>
    <item>
      <title>힙 정렬(Heapsort)</title>
      <link>https://webfirewood.tistory.com/107</link>
      <description>&lt;p&gt;정렬(Sort)은 알고리즘 과목에서 항상 단골로 출제되는 영역입니다. 오늘은 그 중에서 힙 정렬(Heapsort)를 알아보도록 하겠습니다. 우선 이 정렬 알고리즘을 이해하기 위해서는 힙(heap)이라는 독특한 자료구조에 대해 먼저 이해 해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그런데 이 heap을 알려면 우선순위큐(Priority Queue)를 또 먼저 알아야 합니다. 그러면 또 그냥 큐(Queue)가 뭔지도 알아야겠죠. 점점 일이 복잡해 지니까 아주 단순하게 설명하도록 하겠습니다. 각각의 개념에 대해 부족한 설명은 추후에 (가능하다면)포스트 하도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;큐(Queue)는 컴퓨터의 기본적인 자료구조의 일종입니다. 먼저 삽입된 데이터가 먼저 나오는 FIFO(First In First Out)구조로 되어 있는데 화장실 앞에 줄을 선 사람들을 연상하면 쉽게 이해할 수 있습니다. 먼저 줄을 선 사람이 먼저 볼 일을 보고 나오는 것 처럼 먼저 들어간 데이터(혹은 작업)이 먼저 나오는 것이죠.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o6zSG/btqwdhRFHkp/QsKzMAeWHUWycI7umUX5yk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o6zSG/btqwdhRFHkp/QsKzMAeWHUWycI7umUX5yk/img.jpg&quot; data-alt=&quot;차례대로 줄 서 있는 사람들, FIFO 구조이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o6zSG/btqwdhRFHkp/QsKzMAeWHUWycI7umUX5yk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo6zSG%2FbtqwdhRFHkp%2FQsKzMAeWHUWycI7umUX5yk%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;/&gt;&lt;/span&gt;&lt;figcaption&gt;차례대로 줄 서 있는 사람들, FIFO 구조이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그런데 아주 위급한(곧 분출이 시작되는) 상황에 처해 계신 분이 줄의 맨 끝에 섰습니다. 이 분이 화장실 안이 아니라 밖에서 분출을 시작하는 장면을 라이브로 보고 싶은 사람은 드물기 때문이 사람들은 이 위급하신 분께 차례를 양보해 드렸습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 줄을 서 있는 사람에게 차례를 양보하는 것, 다시말해 우선순위를 주는 것 처럼 큐 안에 데이터 들에도 우선순위를 주고 그 우선순위가 높은 데이터가 먼저 처리되도록 하는 자료구조를 우선순위 큐(Priority Queue)라고 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 우선순위 큐는 어떻게 구현할 수 있을까요? 가장 쉽게 생각할 수 있는 방법은 아마 LinkedList나 배열 같은 것을 이용해서 데이터 마다 우선순위를 같이 저장하고 원소를 꺼낼 때 마다 모든 원소를 순회해 우선순위가 가장 높은 원소를 찾는 방법이겠지요. 이렇게 하면 원소를 추가하는데는 O(1), 원소를 꺼내는 데는 O(n)의 시간이 걸리게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러나 언제나 원소를 꺼낼 때 마다 O(n)의 시간이 걸린다면 데이터가 많아지고 꺼내는 횟수가 늘어날 수록 시간이 너무 지연됩니다. 조금 더 빠르게 데이터를 꺼낼 수 있는 방법이 필요했고 그래서 등장한 것이 바로 힙(heap)입니다.&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;힙은 결국 우선순위 큐를 구현하기 위한 자료구조 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;힙, Heap&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;hr&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZKBLO/btqwaF0wkAE/vMTjBgoq1JcebK8kXh1Ps0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZKBLO/btqwaF0wkAE/vMTjBgoq1JcebK8kXh1Ps0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZKBLO/btqwaF0wkAE/vMTjBgoq1JcebK8kXh1Ps0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZKBLO%2FbtqwaF0wkAE%2FvMTjBgoq1JcebK8kXh1Ps0%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;힙(Heap)은 힙 트리(heap tree)라고도 불리는데, 그것은 힙이 트리(Tree), 그 중에서도 완전 이진 트리(Complete binary tree)의 형태를 띄고 있기 때문입니다. 그렇다면 또 완전 이진 트리가 무엇인지 잠깐 살펴 봐야겠죠?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림처럼 트리(Tree) 구조를 가진 것들 중에 각각의 노드가 최대 두 개의 자식 노드만을 가지는 것을 이진트리(binary tree)라고 합니다. 그런데 이 트리의 마지막 레벨을 제외하고 모든 레벨이 완전히 채워져 있고, 마지막 레벨의 노드는 가능한 왼쪽에 있는 것을 완전 이진 트리(complete binary tree)라고 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;완전이진트리 구조를 이용하면 원소를 입력하고 찾는 데 걸리는 시간을 O(logN)만에 수행 가능합니다. 이런 완전이진트리의 특성을 이용하면 우선순위 큐를 아주 쉽게(혹은 빠르게) 구현할 수 있는데 그것이 바로 힙입니다. 그런데 우선순위 큐는 '우선순위'에 따라 출력이 다르게 나온다는 말씀을 이미 드렸습니다. 그렇다면 힙은 우선순위를 어떤식으로 결정하게 될까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0OOYY/btqwcltNuWq/uPsK2dK0Fu4KpoSZ40aYc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0OOYY/btqwcltNuWq/uPsK2dK0Fu4KpoSZ40aYc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0OOYY/btqwcltNuWq/uPsK2dK0Fu4KpoSZ40aYc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0OOYY%2FbtqwcltNuWq%2FuPsK2dK0Fu4KpoSZ40aYc0%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;힙에는 두 가지 종류가 있습니다. 최대 힙(max heap)과 최소 힙(min heap)입니다.&amp;nbsp;최대힙은 부모 노드의 키 값이 자식 노드의 키 값보다 크거나 같은 완전 이진트리를 나타냅니다. 반대로 최소 힙은 부모 노드의 키 값이 자식 노드의 키 값보다 작거나 같은 완전 이진트리를 나타냅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 최대힙 혹은 최소힙을 이용하면 항상 첫번째 노드의 키 값을 최대, 혹은 최소값으로 유지할 수 있게 됩니다. 최대값을 찾는 시간 O(1)로 줄일 수 있는 것이죠. 완전 이진 트리 구조를 하고있기 때문에 삽입에는 O(logN)이 걸립니다. 이전의 배열이나 리스트를 이용하는 방법에 비해 훨씬 빨라진 것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;힙의 구현&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;hr&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csNCjl/btqwb1aYwSH/3WDZtWe0yKuBGAuocBDQPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csNCjl/btqwb1aYwSH/3WDZtWe0yKuBGAuocBDQPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csNCjl/btqwb1aYwSH/3WDZtWe0yKuBGAuocBDQPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsNCjl%2Fbtqwb1aYwSH%2F3WDZtWe0yKuBGAuocBDQPK%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 최대 힙이 실제로 어떤식으로 구현되는지를 보여주고 있습니다. 그림 (a)에서 마지막 레벨에 있는 4가 그림 (b) 처럼 15로 바뀌었다고 해 보겠습니다. 그러면 먼저 15와 상위 노드인 8을 비교합니다. 최대 힙은 항상 상위 노드가 하위 노드보다 더 큰 값을 가지고 있기 때문에 더 큰 값을 가진 15가 상위 노드와 위치를 바꾸게 됩니다. 그 결과가 그림 (c) 입니다. 그리고 다시한 번 상위노드은 14와 비교를 하고 그림(d)와 같이 상위노드와 교환되게 됩니다. 마지막으로 루트인 16과 비교했을 때는 15가 더 작은 숫자이므로 바꿀 이유가 없기 때문에 그림 (d)에서 멈추게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EKIFW/btqweJ05Vhe/6V7xAgqIrhXy3LOkjCE5D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EKIFW/btqweJ05Vhe/6V7xAgqIrhXy3LOkjCE5D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EKIFW/btqweJ05Vhe/6V7xAgqIrhXy3LOkjCE5D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEKIFW%2FbtqweJ05Vhe%2F6V7xAgqIrhXy3LOkjCE5D0%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;반대로 데이터가 삭제되는 경우는 어떨까요? 이 때는 삭제된 노드에 마지막 레벨의 마지막 값을 옮깁니다. 그리고 자식 노드들 중 더 큰 노드와 자리를 바꿉니다. 더이상 자신보다 더 큰 자식 노드 값이 없으면 종료됩니다. 이 삽입 삭제 연산이 모두 O(logN)만에 일어난다는 것을 알 수 있습니다. 힙은 이런식으로 우선순위를 유지하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;힙 정렬(Heapsort)&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;hr&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;힙에 대해서 알았으니 이제 힙 정렬(Heapsort)에 대해서도 조금 감이 잡힐 것입니다. 힙이라는 자료구조는 항상 최 상위 노드값이 최대거나 최소입니다. 최 상위 노드 값을 하나씩 뽑아내면서 정렬을 하는 것이 바로 힙 정렬입니다. 따라서 힙 정렬의 수행 방법은 다음과 같이 구성됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;원소들을 입력하여 최대 힙 구성&lt;/li&gt;
&lt;li&gt;힙에 삭제 연산을 수행하여 얻은 원소를 마지막 자리에 배치&lt;/li&gt;
&lt;li&gt;나머지 원소에 대해서 최대힙 구성&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 과정을 그림으로 보면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4yb0H/btqweKsawOD/OndyxlZXn92Km6BgDXjk31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4yb0H/btqweKsawOD/OndyxlZXn92Km6BgDXjk31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4yb0H/btqweKsawOD/OndyxlZXn92Km6BgDXjk31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4yb0H%2FbtqweKsawOD%2FOndyxlZXn92Km6BgDXjk31%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;(a)에서 1번 단계인 최대힙을 구성합니다. 그리고 최대값인 루트노드를 제거하여 가장 마지막 자리에 넣고 남은 트리에 대해 다시 최대힙을 구성합니다. 그것이 (b) 입니다. 이 과정을 반복하면 결국 마지막에는 (i)와 같이 정렬된 배열을 얻을 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;결국 힙 정렬에서 중요한 것은 정렬 알고리즘 그 자체가 아니라 힙이라는 자료구조의 이해라고 할 수 있을 것 같습니다.&lt;/p&gt;</description>
      <category>algorithm</category>
      <author>북항</author>
      <guid isPermaLink="true">https://webfirewood.tistory.com/107</guid>
      <comments>https://webfirewood.tistory.com/107#entry107comment</comments>
      <pubDate>Thu, 20 Jun 2019 15:35:13 +0900</pubDate>
    </item>
  </channel>
</rss>