<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>heechann</title>
    <link>https://hechan2.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 25 Jul 2026 14:31:59 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>chan_2</managingEditor>
    <image>
      <title>heechann</title>
      <url>https://tistory1.daumcdn.net/tistory/5533630/attach/6db7e573504e4be487e124b5d4d6346e</url>
      <link>https://hechan2.tistory.com</link>
    </image>
    <item>
      <title>4박 5일의 입원 기록 (in 본바움 병원)</title>
      <link>https://hechan2.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케이트보드를 타다 발목에 금이 갔다. MRI 결과를 기다리는 동안 나는 속으로 계속 빌었다. 제발, 인대 손상까지만.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 결과는 골절이었다. 다리를 전문으로 보시는 선생님은 화면을 보시며 담담하게 말씀하셨다. 수술은 월요일에 가능한데, 그때까지 절대 움직이면 안 되니 지금 당장 입원하라고. 토요일 아침이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;골절이라는 사실과 입원이라는 사실이 한꺼번에 밀려오니 잠깐 정신이 아득했다. 그래도 짧은 시간에 생각을 했을 때, 집에 가면 화장실이든 부엌이든 발을 디딜 일이 끝도 없을 테고, 그러다 뼈가 어긋나기라도 하면 더 큰일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 별다른 고민 없이 입원하겠다고 했다. 그런데 입원 상담을 받는데 비용이 생각보다 꽤 나왔다. 실비보험이 없었다면 다쳐도 그냥 자연치유가 정답인가 싶은 금액이었다. 다행히 회사에서 가입해 준 단체 실손보험으로 보장이 되는 부분이었다. 하마터면 생돈이 크게 나갈 뻔한 상황에서, 보험이라는 게 이렇게 중요하구나 하는 걸 처음으로 몸으로 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입원 수속을 밟고 수술 전 필요한 검사를 다 마치니 어느새 오후 12시였다. 병실 침대로 돌아와 앉아 있으니 첫 병원밥이 나왔다. 솔직히 맛있진 않았다. 그렇다고 식욕이 있었던 것도 아니라, 딱히 다른 게 먹고 싶다는 생각조차 들지 않았다. 그 뒤로 이어진 시간은 대부분 기다림이었고, 인내였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 돌이켜보면 굳이 토요일에 입원할 필요가 있었을까 싶기도 하다. 통원하면서 수술 준비를 하고 월요일에 들어와도 괜찮지 않았을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이건 겪어보지 않으면 알 수 없는 종류의 일이었다. 다음엔 이런 건 좀 더 알아보고 결정해야지, 하고 마음에 적어두는 정도로 넘기기로 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;1638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL6ZOT/dJMcag7nj6G/ZBDPzwf7iHbhm3wG5ZICM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL6ZOT/dJMcag7nj6G/ZBDPzwf7iHbhm3wG5ZICM1/img.png&quot; data-alt=&quot;MRI 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL6ZOT/dJMcag7nj6G/ZBDPzwf7iHbhm3wG5ZICM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL6ZOT%2FdJMcag7nj6G%2FZBDPzwf7iHbhm3wG5ZICM1%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;514&quot; height=&quot;509&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;1638&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MRI 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이 사진은 다시 봐도 너무 슬프다. 저렇게 심하게 금이갔다니...&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;링거를 달고 사는 삶&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토요일 저녁, 링거를 맞기 위해 꽤 굵은 바늘이 왼쪽 팔에 깊이 들어왔다. 링거를 맞아본 게 처음이었는데, 5일 동안 가장 불편했던 걸 하나 꼽으라면 단연 이 링거였다. 어디를 가든 매달고 다녀야 하는 그 존재감이 삶의 질을 야금야금 갉아먹었다. 한 번은 줄로 피가 역류하는 걸 보고 깜짝 놀라 간호사를 호출했는데, 자연스러운 현상이라는 말에 괜히 부끄러웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병원의 아침은 정말 빨랐다. 왜 굳이 5시에 깨워서 검사를 하는지는 지금도 잘 모르겠다. 5시에 한 번 깨고, 7시 반쯤 아침밥에 또 한 번 깨고. 밤 12시에 잠들었다면 피곤한 게 당연한 구조인데, 이상하게 몸은 피곤함을 잘 느끼지 못했다. 링거 때문인지 몸이 제정신이 아닌 것 같기도 했다. 대신 낮에 할 게 없어서 틈틈이 잤다. 밥을 먹고 가만히 누워 있으면 저절로 눈이 감기고, 감으면 그대로 잠드는 그런 매직. 그렇게 하루가 뭉근하게 흘러갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머리 감는 것부터 얼굴을 닦는 것까지 전부 불편했고, 샤워는 아예 불가능했다. 그러니 온몸이 씻고 싶다고 아우성이었다. 머리도 기름지고, 얼굴도 기름지고. 결국 아빠 찬스를 써서 5일 동안 머리를 세 번 감을 수 있었다. 혼자서는 도저히 못 하겠던 일이었다. 그런데 옆자리 아저씨는 허리 수술에 무릎 수술까지 하시고도 혼자 머리를 감고 오셨다. 그 모습을 보면서 정말 대단하다는 생각이 절로 들었다.&lt;/p&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qn3hz/dJMcahLZQ3h/9u6PBrDATWP1hA2YeNUfI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qn3hz/dJMcahLZQ3h/9u6PBrDATWP1hA2YeNUfI0/img.png&quot; data-alt=&quot;소소한 선물&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qn3hz/dJMcahLZQ3h/9u6PBrDATWP1hA2YeNUfI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqn3hz%2FdJMcahLZQ3h%2F9u6PBrDATWP1hA2YeNUfI0%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;483&quot; height=&quot;644&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&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 style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;같은 파크에서 타는 친한 아저씨가 커피랑 쿠키를 사다 주셨다. 딸아이랑 같이 오셨는데, 인사하는게 너무 귀여웠다 ㅋㅋ.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;나도 결혼하면 딸을 낳아야지... 아들은 나처럼 말썽이겠지?&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;창밖의 거리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 배정받은 자리는 창문에 딱 붙어 있는 자리였다. 8층이었고 주변에 높은 건물이 없어서 밖으로 보이는 풍경이 꽤 좋았다. 그런데 책을 읽고, 블로그 글을 쓰다가 문득 고개를 들어 밖을 볼 때마다 기분이 그리 좋지만은 않았다. 창밖으로는 내가 평소 자주 돌아다니던 거리가 보였고, 그 거리를 자유롭게 오가는 사람들이 보였다. 내가 할 수 있던 걸 지금은 못 한다는 사실이, 그 장면 앞에서는 유난히 받아들이기 어려웠다. 하필 날씨는 또 어찌나 좋던지. 지나다니는 사람들과 그 풍경을 보고 있으면 조금은 우울해졌다. 좋아하는 것들을 못 하니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 문득, 병원에만 있어야 하는 모든 환자들이 어떤 기분일지 조금씩 공감이 되기 시작했다. 드라마에서 창밖을 하염없이 바라보던 환자들의 장면. 예전엔 그냥 지나쳤던 그 장면들이, 이제는 다시 본다면 공감이 될 것 같다. 같은 창인데 누군가에겐 풍경이고 누군가에겐 누리지 못하는 그림이라는 걸, 나는 그 8층 창가에서 처음 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7rYWc/dJMcahSHWiB/owSlYMXewlyAA9bnpNeJgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7rYWc/dJMcahSHWiB/owSlYMXewlyAA9bnpNeJgK/img.png&quot; data-alt=&quot;창밖 풍경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7rYWc/dJMcahSHWiB/owSlYMXewlyAA9bnpNeJgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7rYWc%2FdJMcahSHWiB%2FowSlYMXewlyAA9bnpNeJgK%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;539&quot; height=&quot;404&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&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;h2 data-ke-size=&quot;size26&quot;&gt;사소한 것의 즐거움&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병원은 정말 재미가 없다. 할 게 없으니 누워서 휴대폰만 만지게 되는데, 그마저도 딱히 재미있지 않다. 일요일도 그렇게 무료하게 흘러갔다. 그래도 그날은 소소하게 웃긴 사건이 하나 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 병실은 4인실이었는데, 나를 빼면 다들 이미 수술을 받으신 분들이었다. 나는 당연히 다들 걸어 다녀도 되는 줄 알았다. 워낙 잘 돌아다니셨으니까. 그래서 나도 아프지 않은 선에서 슬슬 돌아다녔다. 그런데 들어오신 간호사님의 말이 놀라웠다. 이 방 사람들 전부 움직이면 안 되는 상태라는 것이었다. 스물여덟에 잔소리를 듣고 있는데, 옆자리 분은 마흔다섯이셨다. 다 같이 한바탕 &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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfq55q/dJMcabEZFWN/6Xm4mbU56ZGL7EHAtWvBD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfq55q/dJMcabEZFWN/6Xm4mbU56ZGL7EHAtWvBD0/img.png&quot; data-alt=&quot;강제 휠체어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfq55q/dJMcabEZFWN/6Xm4mbU56ZGL7EHAtWvBD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfq55q%2FdJMcabEZFWN%2F6Xm4mbU56ZGL7EHAtWvBD0%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;355&quot; height=&quot;473&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&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;h2 data-ke-size=&quot;size26&quot;&gt;수술실 앞에서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일요일 저녁, 잠을 자려고 눈을 감아도 잠이 잘 오지 않았다. 다음 날이 수술이라는 사실이 생각보다 크게 다가왔던 것 같다. 그날 밤 잠을 설치고, 다음 날 아침 일어나 11시쯤 수술실로 들어갔다. 들어가자마자 이름과 어디를 수술하는지 물어보시는데, 내가 제정신인지 확인하려는 질문 같았다. 수술실 안에 들어서니 긴장이 훅 올라왔다. 잘못되면 어쩌지, 마취 주사가 아파서 내가 움직이면 어쩌지, 하는 쓸데없는 생각들이 머릿속을 가득 채웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 겁먹었던 것과 달리 마취 주사는 아프지 않았다. 주사와 거의 동시에 수면 마취가 진행되어서, 눈을 감았다 떴더니 이미 수술이 끝나 있었다. 끝나고도 이름을 한 번 더 물어봐 주셨고, 그렇게 원래 병실로 돌아왔다. 하반신에 감각이 하나도 없는 게 신기했다. 다리를 양옆으로 벌리고 있는 줄 알았는데 사진을 찍어보니 멀쩡한 정자세였다. 감각과 현실이 이렇게 다를 수 있다는 게 웃겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;살면서 수술이라는 걸 한번 언제 해보게 될까, 가끔 막연히 떠올리곤 했는데 이렇게 갑작스럽게 겪고 나니 신기하면서도 어딘가 씁쓸했다. 전날 밤부터 그렇게 걱정했는데, 막상 겪어보니 걱정할 정도는 아니었다. 아마 처음으로 마취를 하고 처음으로 수술대에 올랐기 때문에 그렇게 마음을 졸였던 것 같다. 그래서 처음 수술실에 들어가는 모든 사람에게 말해주고 싶다. 그렇게까지 걱정할 것 없다고. 눈을 한 번 감았다 뜨면 어느새 다 지나가 있을 거라고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NS8hY/dJMcacqsSCW/qFZvpnLKLUB2EKL5dV7sf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NS8hY/dJMcacqsSCW/qFZvpnLKLUB2EKL5dV7sf1/img.png&quot; data-alt=&quot;하반신 마취 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NS8hY/dJMcacqsSCW/qFZvpnLKLUB2EKL5dV7sf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNS8hY%2FdJMcacqsSCW%2FqFZvpnLKLUB2EKL5dV7sf1%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;368&quot; height=&quot;491&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&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;h2 data-ke-size=&quot;size26&quot;&gt;병실에서 업무를 보다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월요일은 수술도 있고 마취도 해야 해서 아까운 휴가를 하루 썼다. 다행히 토요일에 미리 매니저님께 상황을 말씀드렸더니, 움직이기 편해질 때까지 재택을 허락해 주셨다. 그래서 화요일은 병실에 앉아 일을 했는데, 이게 여간 불편한 게 아니었다. 침대에 앉아 다리를 뻗고 일하다 보면 발과 다리가 저려오고, 그렇다고 수술한 발을 바닥에 내리면 피가 쏠려 열감이 올라왔다. 다시 다리를 올리고, 이 자세 저 자세 다 취해봤지만 끝내 편안한 자세는 찾지 못했다. 그날 해야 할 일은 그래도 다 잘 마무리했다. 다만 앞으로 병원에 있을 때는 그냥 마음 편히 휴가를 쓰기로 마음먹었다. 아무래도 병실은 일하기에 적합한 공간은 아닌 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;병원 후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 내가 느낀 것들에 대한 이야기였다. 그래도 병원에 대한 후기를 조금 남겨두면 나중에 누군가에게 도움이 되지 않을까 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 입원한 본바움병원은 집에서 5분도 안 되는 거리에 있어서 골랐다. 최근에 개원한 정형외과 전문 병원이라, 가깝고 내게 필요한 곳이라는 이유만으로 별 고민 없이 선택했다. 지금 생각하면 조금 더 알아보고 갔다면 더 나은 조건으로 입원할 수 있었을지도 모른다. 다만 나는 번거로운 걸 싫어하고 단순하게 해결하고 싶은 사람이라, 이번엔 그 성향대로 움직였다.&lt;/p&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;불편했던 점도 솔직히 있었다. 병실이 조금 좁게 느껴졌다. 옆 사람과 커튼을 쳐도 틈으로 서로가 보일 만큼 가까웠고, 두 침대에 보호자 의자를 다 놓으면 지나갈 길이 있을까 싶을 정도였다. 처음엔 몰랐는데 오래 지내다 보니 조금 답답하게 느껴지기도 했다. 물리치료도 그랬다. 10만 원짜리 비급여 치료를 안내받았을 때, 당시엔 그게 유일한 선택지인 줄로만 알았는데 나중에 알고 보니 다른 선택지도 있었던 모양이다. 이런 점들이 조금 아쉬웠다. 그래서 앞으로는 물리치료도 건강보험으로 처리되는 선에서 받아볼 생각이다. 물론 이건 어디까지나 내 개인적인 경험이고, 사람마다 느끼는 건 다를 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 병원이 세로로 긴 구조라 검사를 받을 때마다 층을 옮겨 다녀야 했다. 환자가 적어서 다행이지, 많았다면 엘리베이터를 기다리느라 시간을 다 썼을 것 같다. CT, X-ray, MRI를 찍으러 5층에 갔다가 3층으로 내려와 진료를 보고 또 다른 층으로 이동하는 동선을 한 번 겪고 나면, 조금 번거롭다는 생각이 들긴 했다. 적다 보니 결국 대부분 개인적인 불편함이긴 하지만, 그래도 병원을 고르기 전에 이런 정보를 미리 알고 간다면 다른 곳과 비교하기엔 좋지 않을까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다시, 창가에서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4박 5일. 짧다면 짧고 길다면 긴 시간이었다. 스케이트보드 위에서 즐거워 하던 나는 잠시 8층 창가에 묶여, 그동안 당연하게 여기던 것들을 하나씩 다시 바라봤다. 마음대로 걷는 일, 뜨거운 물로 개운하게 씻는 일, 창밖의 거리를 아무렇지 않게 걸어 다니는 일. 전부 대단할 것 없어 보였던 일상인데, 못 하게 되고 나서야 그게 얼마나 중요한 것들이었는지 알게 됐다.&lt;/p&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;</description>
      <category>일상</category>
      <category>발목 골절</category>
      <category>본바움병원</category>
      <category>수술</category>
      <category>수술 후기</category>
      <category>일상</category>
      <category>입원</category>
      <category>입원 기록</category>
      <author>chan_2</author>
      <guid isPermaLink="true">https://hechan2.tistory.com/38</guid>
      <comments>https://hechan2.tistory.com/38#entry38comment</comments>
      <pubDate>Thu, 23 Jul 2026 23:30:30 +0900</pubDate>
    </item>
    <item>
      <title>천천히 배운 4년, 빠르게 배운 석 달</title>
      <link>https://hechan2.tistory.com/37</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_4057.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcR5EX/dJMcagzuzWj/4GkPWOsutyF3paXoKoOKLK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcR5EX/dJMcagzuzWj/4GkPWOsutyF3paXoKoOKLK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcR5EX/dJMcagzuzWj/4GkPWOsutyF3paXoKoOKLK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcR5EX%2FdJMcagzuzWj%2F4GkPWOsutyF3paXoKoOKLK%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;475&quot; height=&quot;633&quot; data-filename=&quot;IMG_4057.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 발목 골절 진단을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어제 파크에서 드랍인 이후 단계에서 넘어졌다. 램프에서 내려가는 순간, 옆 기물에서 다른 사람이 출발하는 게 보였다. 동선이 겹치겠다는 생각이 스쳤고, 반사적으로 속도를 죽여야겠다는 판단이 몸보다 먼저 나갔다. 그 결과, 중심을 잃었고, 보드에서 발을 빼는 순간 발이 바닥에 끌렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인대 부분 파열, 그리고 발목 뼈에 간 금. 어젯밤에는 '인대가 좀 다쳤겠네' 정도로 생각하고 잠들었는데, 아침에 병원에서 검사를 받아보니 뼈까지 금이 가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입원실에 누워서 처음 든 생각은 '이제 2~3개월 동안 보드를 못 타는데 어떡하지'였다. 두 번째 생각은 '발이 나았을 때를 위해 코어 운동을 해둬야겠다'였다. 누가 보면 미쳤다고 할지도 모른다. 하지만 그만큼 4월부터 7월까지의 석 달이 즐거웠다. 24살의 나를 넘어서 계속 늘고 있다는 감각이, 시간 가는 줄 모르게 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;천천히, 안전하게 배우던 시절&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케이트보드는 대학교 때 시작했다. 지금보다 시간이 훨씬 많았던 2021년에는 정말 열심히 탔다. 다만 방식이 달랐다. 천천히, 안전하게. 넘어지지 않는 선 안에서만 연습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래 탔고 시간도 많이 썼지만, 실력이 느는 속도는 더뎠다. 그 시절 영상을 지금 다시 봐도 '엄청나다'는 느낌은 들지 않는다. 2023년까지 그렇게 타다가 2024년부터는 한 달에 한두 번으로 줄었고, 취업 준비를 시작하면서 2년 가까이 보드를 놓았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빠르게, 과감하게 배우기로 한 올해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년, 취업을 하고 나서 보드를 다시 타기 시작했다. 이번에는 방식을 바꿨다. 대학생 때와 달리 이제는 시간이 부족했고, 20대가 가기 전에 더 화려하고 위험한 기술을 배워두고 싶었다. 그래서 대학교 때도 안 하던 것들을 28살에 하기 시작했다. 좀 더 빠르게, 좀 더 과감하게. 개인 강습도 두 번 받았고, 그 덕에 오랫동안 두려워하던 드랍인도 해낼 수 있었다.&lt;/p&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-18 오후 10.47.13.png&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HfrSU/dJMcadixjk8/fa1sVcbWLzRq9r2u0npGB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HfrSU/dJMcadixjk8/fa1sVcbWLzRq9r2u0npGB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HfrSU/dJMcadixjk8/fa1sVcbWLzRq9r2u0npGB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHfrSU%2FdJMcadixjk8%2Ffa1sVcbWLzRq9r2u0npGB0%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;680&quot; height=&quot;382&quot; data-filename=&quot;스크린샷 2026-07-18 오후 10.47.13.png&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;511&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다치고 나서 보이는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다치고 나니 보이는 게 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전처럼 안전하게 탔다면 다치지 않았을 것이다. 그건 분명하다. 하지만 그렇게 탔다면 실력도 그 자리에 머물러 있었을 것이다. 2021년의 나는 훨씬 많은 시간을 쓰고도 천천히 늘었고, 올해의 나는 석 달 만에 4년 치를 따라잡고 넘어섰다. 빠른 속도에는 처음부터 대가가 붙어 있었고, 오늘 그 대가를 받았을 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 후회하냐고 묻는다면, 후회는 하지 않는다. '안전하게 탔다면 안 다쳤을 텐데'라는 생각은 분명 스쳐 지나가고 있다. 하지만 그 뒤에 곧바로 따라온 생각은 이거였다. 예전 처럼 탔다면, 나는 아직도 두려워하던 것들 앞에 그대로 서 있었겠지.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서, 앞으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 정해져 있는 것 같다. 30대가 되기 전까지는 젊음이라는 무기를 쓰기로 했다. 골절이 다 낫고, 다음에는 더 크게 다칠 수도 있다. 그래도 28년을 살아오며 배운 게 하나 있다면, 결국 오래 남는 후회는 다친 기억이 아니라 그 시절에 시도하지 않은 기억이라는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 당분간은 장난감을 압수당한 아이의 기분으로 석 달을 버텨야 한다. 그게 지금은 제일 슬프다. 그러니 발이 붙는 동안 코어 운동을 하며 더 높은 알리를 뛸 준비를 해 놓을 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 램프에서 알리 하이를 하고싶다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AIX19/dJMcacDWYEd/QaZcK0iKHfwjxM8MVZvVx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AIX19/dJMcacDWYEd/QaZcK0iKHfwjxM8MVZvVx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AIX19/dJMcacDWYEd/QaZcK0iKHfwjxM8MVZvVx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAIX19%2FdJMcacDWYEd%2FQaZcK0iKHfwjxM8MVZvVx1%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;680&quot; height=&quot;383&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>일상</category>
      <category>SK8</category>
      <category>골절</category>
      <category>스케이트 보드</category>
      <category>일상</category>
      <author>chan_2</author>
      <guid isPermaLink="true">https://hechan2.tistory.com/37</guid>
      <comments>https://hechan2.tistory.com/37#entry37comment</comments>
      <pubDate>Sat, 18 Jul 2026 22:55:29 +0900</pubDate>
    </item>
    <item>
      <title>『시시한 사람이면 어때서』를 읽고</title>
      <link>https://hechan2.tistory.com/36</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1784035744160&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;시시한 사람이면 어때서 - 교보문고&quot; data-og-description=&quot;상처받기 싫어서 아무것도 하고 싶지 않은 날들, 나누고 싶은 공감의 한마디 모든 날이 어둡고 축축하고 긴 터널처럼 느껴졌을 때, 남들은 다 잘나가는데 나만 제자리인 것 같아 조바심이 날 때,&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000001811716&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000001811716&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cHJHq5/dJMb86n9X79/9wJPBA0eFs90vkLKoBqDMK/img.jpg?width=458&amp;amp;height=751&amp;amp;face=0_0_458_751,https://scrap.kakaocdn.net/dn/RsIXX/dJMb81G9Q2H/7ICrJsDRfjfPDqO4ZJkIX0/img.jpg?width=458&amp;amp;height=751&amp;amp;face=0_0_458_751,https://scrap.kakaocdn.net/dn/csM9Ny/dJMb9eT1IFv/APWsRKoA5d05gpOtitOv11/img.jpg?width=400&amp;amp;height=656&amp;amp;face=0_0_400_656&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001811716&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000001811716&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cHJHq5/dJMb86n9X79/9wJPBA0eFs90vkLKoBqDMK/img.jpg?width=458&amp;amp;height=751&amp;amp;face=0_0_458_751,https://scrap.kakaocdn.net/dn/RsIXX/dJMb81G9Q2H/7ICrJsDRfjfPDqO4ZJkIX0/img.jpg?width=458&amp;amp;height=751&amp;amp;face=0_0_458_751,https://scrap.kakaocdn.net/dn/csM9Ny/dJMb9eT1IFv/APWsRKoA5d05gpOtitOv11/img.jpg?width=400&amp;amp;height=656&amp;amp;face=0_0_400_656');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;시시한 사람이면 어때서 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;상처받기 싫어서 아무것도 하고 싶지 않은 날들, 나누고 싶은 공감의 한마디 모든 날이 어둡고 축축하고 긴 터널처럼 느껴졌을 때, 남들은 다 잘나가는데 나만 제자리인 것 같아 조바심이 날 때,&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;책을 만난 계기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 링크드인 계정을 만들어 가끔 앱에 들어가 피드를 읽는다. 어느 날 누군가 읽은 책 하나가 눈에 밟혔다.&lt;/p&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;평소 책을 즐겨 읽는 편은 아니지만, 이상하게 읽어보고 싶다는 생각이 들었다. 순간의 이끌림이었고, 그 자리에서 e-book으로 바로 샀다. 이렇게 책을 충동적으로 산 것도, 다 읽고 블로그에 독후감을 남기고 싶어진 것도 이 책이 처음이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;뜻밖의 발견 &amp;mdash; 나는 에세이를 좋아했다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 읽고 놀란 사실은, 내가 에세이를 좋아한다는 것이었다. 군대에서는 소설이나 자기계발서를 주로 읽어서 '내가 에세이를 읽을 날이 올까?' 싶었는데, 이 책은 마지막 장이 다가오는 게 싫어질 만큼 아쉬웠다. 예전에 『지적 대화를 위한 넓고 얕은 지식』을 읽을 때도 재밌었지만 이 정도는 아니었다. 글마다 밑줄을 긋고 싶었고, 한 챕터를 넘길 때마다 공감되는 이야기가 많아 내가 어떻게 살아왔는지 되돌아볼 수 있는 시간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가장 깊이 남은 생각 &amp;mdash; 하지 않은 선택의 가치&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직장을 다니며 동료나 친구와 이야기하다 보면 '그때 내가 이렇게 했다면' 하는 가정을 자주 한다. 이 책의 저자도 비슷한 상상을 한다. 하지만, 비슷한 생각에서 저자는 다른 지점에 도달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 그토록 소심한 아이가 아니었다면 결코 알 수 없었을 것들이 있다. 자기가 좋아하는 걸 말하는 일에도 큰 용기가 필요한 사람이 있다는 것. 겪어본 사람만 알고, 겪지 않은 사람은 존재조차 모르는 두려움. &lt;br /&gt;그 앎이 또 다른 방향에서 삶을 떠받쳐왔다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 대목에서 머리를 망치로 한 대 맞은 기분이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 과거의 나에 대해 후회도 해보고, 그 때 했던 선택들에 대해 인정까지도 해봤다. 하지만 그 과정에서 내가 느낀 감정까지 들여다보고 거기서 얻었던 관점에 대해서는 깊이 생각해 본적은 없었다. 이번에 처음으로 그 감정들을 떠올려봤는데, 어쩌면 그 때 느꼈던 감정들이야말로 지금의 나를 만든 필수 요소가 아니었을까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;후기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 소재가 한두 챕터뿐이었다면 추천하지 않았을 것이다. 하지만 거의 모든 챕터가 공감됐다. 출퇴근길 지하철에서 e-book으로 읽어가다 마지막 페이지를 넘길 때, 책이 끝나는 게 이렇게 아쉬웠던 건 처음이다. 기록을 하다 보니 다시 한번 읽고 싶어졌다.&lt;/p&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;</description>
      <category>책</category>
      <category>독후감</category>
      <category>시시한 사람이면 어때서</category>
      <author>chan_2</author>
      <guid isPermaLink="true">https://hechan2.tistory.com/36</guid>
      <comments>https://hechan2.tistory.com/36#entry36comment</comments>
      <pubDate>Tue, 14 Jul 2026 22:43:11 +0900</pubDate>
    </item>
    <item>
      <title>무신사 AI Rookie 전형 지원 회고</title>
      <link>https://hechan2.tistory.com/35</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HfaIB/dJMcaaxM7bv/2o4cX8KvShQG7sBrRpYn80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HfaIB/dJMcaaxM7bv/2o4cX8KvShQG7sBrRpYn80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HfaIB/dJMcaaxM7bv/2o4cX8KvShQG7sBrRpYn80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHfaIB%2FdJMcaaxM7bv%2F2o4cX8KvShQG7sBrRpYn80%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;1366&quot; height=&quot;461&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1월 말부터 2월 말까지 서류 포함 총 4단계에 걸친 과정이 끝났다. 각 전형마다 결과가 바로바로 나와 되게 빠르게 지나갔던 것 같다. 결과는 운이 좋게도 합격이었고, 좋은 기억이었던 만큼 이전처럼 준비했던 내용 위주로 기록하기보다 생각이나 느꼈던 감정 위주의 일기 형식으로 기록해보려고 한다. AI는 절대 이렇게 쓰지 않겠지? 싶은 날것의 글을 한번 남겨봐야지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 내가 지원했던 공고다. (언제 사라질지 모른다. 안 사라질 수도?)&lt;/p&gt;
&lt;figure id=&quot;og_1773546887431&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;musinsacareers&quot; data-og-host=&quot;www.musinsacareers.com&quot; data-og-source-url=&quot;https://www.musinsacareers.com/ko/musinsa-rookie&quot; data-og-url=&quot;https://www.musinsacareers.com/ko/musinsa-rookie&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/WzbGM/dJMb8T9X8yS/QNUp2LyJOBzz8ktbyh6ut1/img.jpg?width=566&amp;amp;height=566&amp;amp;face=0_0_566_566,https://scrap.kakaocdn.net/dn/Dx3tJ/dJMb8SXwrb8/mgKz98KG8eSvkIdlbKNVgk/img.jpg?width=566&amp;amp;height=566&amp;amp;face=0_0_566_566&quot;&gt;&lt;a href=&quot;https://www.musinsacareers.com/ko/musinsa-rookie&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.musinsacareers.com/ko/musinsa-rookie&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/WzbGM/dJMb8T9X8yS/QNUp2LyJOBzz8ktbyh6ut1/img.jpg?width=566&amp;amp;height=566&amp;amp;face=0_0_566_566,https://scrap.kakaocdn.net/dn/Dx3tJ/dJMb8SXwrb8/mgKz98KG8eSvkIdlbKNVgk/img.jpg?width=566&amp;amp;height=566&amp;amp;face=0_0_566_566');&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;musinsacareers&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.musinsacareers.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서류 전형&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2829&quot; data-origin-height=&quot;867&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/naWBr/dJMcafZ8pD4/6XTVv98ckOGrrPU3E01j0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/naWBr/dJMcafZ8pD4/6XTVv98ckOGrrPU3E01j0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/naWBr/dJMcafZ8pD4/6XTVv98ckOGrrPU3E01j0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnaWBr%2FdJMcafZ8pD4%2F6XTVv98ckOGrrPU3E01j0k%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;2829&quot; height=&quot;867&quot; data-origin-width=&quot;2829&quot; data-origin-height=&quot;867&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 전형까지만 해도 정말 후회하고 싶지 않다는 생각으로 지원했다. 내가 될 것이다라고 생각은 하지 않았다. 그냥 늘 그랬던 것처럼 지원했다. 지원하며 느낀 건 일단 자기소개서 작성이 없어서 좋았고, AI를 1월 동안 적극적으로 사용하려고 했는데 이런 부분이 평가 요소에 있어서 좋았다. 지원하고 며칠 후 서류 통과 이메일이 날아왔다. 찾아보니 적부라는 소문을 들었고, 그냥 열심히&amp;nbsp;코테나&amp;nbsp;준비해야지라는&amp;nbsp;생각을&amp;nbsp;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1차 코딩테스트 : 60분 구현 2문제&lt;/h2&gt;
&lt;figure id=&quot;og_1773547194366&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;2026-musinsa-rookie/1차 문제 at main &amp;middot; musinsatech/2026-musinsa-rookie&quot; data-og-description=&quot;2026 무신사 AI Native 개발자 채용 2차 시험 안내. Contribute to musinsatech/2026-musinsa-rookie development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/musinsatech/2026-musinsa-rookie/tree/main/1%EC%B0%A8%20%EB%AC%B8%EC%A0%9C&quot; data-og-url=&quot;https://github.com/musinsatech/2026-musinsa-rookie/tree/main/1%EC%B0%A8%20%EB%AC%B8%EC%A0%9C&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dm4TzG/dJMb8Rj0Apu/o06cYDEDvWihXydBF6Tsz1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/yLUrR/dJMb8VNt1R8/NCBS9n01oJlGPjZ4wNspy1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/musinsatech/2026-musinsa-rookie/tree/main/1%EC%B0%A8%20%EB%AC%B8%EC%A0%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/musinsatech/2026-musinsa-rookie/tree/main/1%EC%B0%A8%20%EB%AC%B8%EC%A0%9C&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dm4TzG/dJMb8Rj0Apu/o06cYDEDvWihXydBF6Tsz1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/yLUrR/dJMb8VNt1R8/NCBS9n01oJlGPjZ4wNspy1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2026-musinsa-rookie/1차 문제 at main &amp;middot; musinsatech/2026-musinsa-rookie&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2026 무신사 AI Native 개발자 채용 2차 시험 안내. Contribute to musinsatech/2026-musinsa-rookie development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 한 줄 평을 말하자면 물리적인 코드량이 너무 많았다. 한 번 실수하게 되면 절대 두 문제를 모두 풀지 못하는 유형의 코딩테스트였다. 난 불행히도 1번 문제에서 실수를 남발했고 2번 문제를 보았을 때 20분이라는 시간이 남게 되었다. 열심히 풀어보려 했지만 입력하는 부분을 다 작성하고 조건문을 작성하려 시간을 봤는데 1분이 남아있었고, 그렇게 코딩테스트가 끝났다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3일 후, 그리팅 카카오 채널에서 무신사 결과가 나왔다고 알림이 왔다. 생각보다 속도가 빠른데? 싶었고 반쯤 기대하고 봤는데 다행히&amp;nbsp;합격했다.&amp;nbsp;생각보다&amp;nbsp;커트라인이&amp;nbsp;널널해서&amp;nbsp;다행이라는&amp;nbsp;생각이&amp;nbsp;들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2차 코딩테스트 : AI 사용 가능한 3시간 코딩테스트&lt;/h2&gt;
&lt;figure id=&quot;og_1773547221122&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;2026-musinsa-rookie/PROBLEM.md at main &amp;middot; musinsatech/2026-musinsa-rookie&quot; data-og-description=&quot;2026 무신사 AI Native 개발자 채용 2차 시험 안내. Contribute to musinsatech/2026-musinsa-rookie development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/musinsatech/2026-musinsa-rookie/blob/main/PROBLEM.md&quot; data-og-url=&quot;https://github.com/musinsatech/2026-musinsa-rookie/blob/main/PROBLEM.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/wN8Ib/dJMb89yceC3/rFJAipOcseFOjItIo1xs5k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/d7Q4Iz/dJMb81fRhj9/hYhj4J0ZeodrCIjaI0Tkn0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/musinsatech/2026-musinsa-rookie/blob/main/PROBLEM.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/musinsatech/2026-musinsa-rookie/blob/main/PROBLEM.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/wN8Ib/dJMb89yceC3/rFJAipOcseFOjItIo1xs5k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/d7Q4Iz/dJMb81fRhj9/hYhj4J0ZeodrCIjaI0Tkn0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2026-musinsa-rookie/PROBLEM.md at main &amp;middot; musinsatech/2026-musinsa-rookie&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2026 무신사 AI Native 개발자 채용 2차 시험 안내. Contribute to musinsatech/2026-musinsa-rookie development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 1차보다는 자신이 있었다. 1월부터 진행했던 프로젝트에서 AI를 적극적으로 활용하고 있었고, 어떤 문제가 나올지는 모르지만 대충 Spec 기반으로 문제를 풀어나가야지라는 생각을 가지고 미리 Skills 2개를 만들어 놓았다. 실제로 내가 만들어 놓은 Skills를 적용할 수 있는 문제가 나왔고, 3시간 동안 목표로 했던 구현을 모두 진행하진 못했지만, 문제에서 제시했던 기본 조건들은&amp;nbsp;모두&amp;nbsp;충족하게&amp;nbsp;만들었다고&amp;nbsp;생각했다. &lt;br /&gt;&lt;br /&gt;확실히 1차 코테보다 아쉬웠던 것은 내가 그만큼 더 자신 있어 하던 영역이라 그랬던 것일까? 끝나고 테스트 코드를 제대로 다 작성하지 못한 것, 문서 작성도 너무 급하게 한 것들이 머릿속에 맴돌았다. 뭐든 내가 열심히 한 것에는 역시 마음이 쓰이나 보다. &lt;br /&gt;&lt;br /&gt;5일 뒤, 밥 먹고 공부하러 가는 엘리베이터 안에서 그리팅 카카오톡 채팅을 보게 되었고 바로 확인했는데.. 나이스...! 또 면접까지 왔다. 뭔가 너무 빠르게 지나가서 그런지 내가 했던 노력들은 잊어버리고 조금은 쉽게 왔다는 생각이 들었다. 그래도 참&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3차 면접 : 면접관 1명과 1시간의 인터뷰 (with 나를 강하게 만들어준 채널톡)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12월 한 달 동안 채널톡이 나를 강하게 만들어줬기 때문에 자신감이 있었다. 아마 채널톡에 지원하지 않았거나 혹은 과정 중에 열심히 준비하지 않았다면 이런 자신감이 존재하지 않았을 것이다. 면접 준비는 생각보다 수월했다. 한 번 진행해 보았던 힘들었던 기술면접이 아마도 똑같이 무신사에서 진행되지 않을까?라는 생각이 들었고, 그래서 그냥 그대로 준비했다. 이때 했던 생각은 이거보다&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;그렇게 면접 당일이 되었고 긴장 반 기대 반으로 일대일 면접에 들어갔고, 면접이 끝났을 땐 생각보다 내가 경험 정리를 잘해놔서 길게 말하는데도 말을 정말 잘했다는 느낌이 들었고, 질문 대비를 정말 잘해왔다는 생각이 들었다. 예상을 벗어난 질문은 2개 정도 왔는데 모두 그 자리에서 임기응변으로 대처할 수 있는 수준이었고, 집에 돌아가며 내가 잘 말했는지 AI에게 질문을&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;근데 웃기게도 면접은 잘 보거나 못 보거나 사실 결과는 항상 잘 모르겠다. 이번에도 정말 잘 봤다고 생각이 들었는데 결과가 나오기까지 마음을 졸이며 기다렸던 것 같다. 실제로 꿈도 2번 정도 꿨는데 한 번은 광탈하는 꿈이었고, 한 번은 예비 번호를 받는&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 결과 : 무신사 AI Rookie 합격&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3월 3일 휴일이 지난 다음 날 5시, 침대에 누워서 유튜브를 보고 있었다. 토요일에 진행하던 팀 프로젝트 최종 발표를 마친 뒤부터 결과가 나오기까지 조금만 휴식을 취하자는 생각으로 3일 정도 힐링을 하고 있었다. 그때, 역시 그리팅에서 무신사 이메일이 발송되었다는 알림이 왔고, 그대로 바로 Gmail을 켜서 메일 제목을 보는데..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;277&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/psDga/dJMcaaYPU0i/r6bRy1uTOm7uIqJAQ9KSK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/psDga/dJMcaaYPU0i/r6bRy1uTOm7uIqJAQ9KSK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/psDga/dJMcaaYPU0i/r6bRy1uTOm7uIqJAQ9KSK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpsDga%2FdJMcaaYPU0i%2Fr6bRy1uTOm7uIqJAQ9KSK0%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;1124&quot; height=&quot;277&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;277&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;[MUSINSA] 안희찬님, 3차 인터뷰 합격 및 입사&amp;nbsp;관련&amp;nbsp;안내드립니다.&quot; &lt;br /&gt;&lt;br /&gt;지금 이 글을 쓰다가도 저 순간의 그 기쁜 감정이 떠오르고 또, 1년간의 취준이 머릿속에 스쳐 지나가며 눈에 눈물이 맺히는 기분? &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업은 정말 아이러니한 것 같다. 취준을 시작하며 무신사는 신입을 안 뽑기로 유명해 가고 싶은 기업 리스트에서 지워놓고 준비하고&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;1년이라는 취준 기간 동안 가장 변한 관점이 있다면 실패와 운에 대해 내가 갖는 생각이다. 감사합니다 무신사. 나를 데려가 줘서.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>취준</category>
      <category>무신사</category>
      <category>무신사 AI Rookie</category>
      <category>무신사 채용</category>
      <category>취준일기</category>
      <category>취준회고</category>
      <category>합격후기</category>
      <author>chan_2</author>
      <guid isPermaLink="true">https://hechan2.tistory.com/35</guid>
      <comments>https://hechan2.tistory.com/35#entry35comment</comments>
      <pubDate>Sun, 15 Mar 2026 13:14:28 +0900</pubDate>
    </item>
    <item>
      <title>채널톡 Software Engineer 전형 지원 회고</title>
      <link>https://hechan2.tistory.com/34</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beoOG8/dJMcab4mPdg/lGXQyJ4ZOkQazklSQo7cV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beoOG8/dJMcab4mPdg/lGXQyJ4ZOkQazklSQo7cV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beoOG8/dJMcab4mPdg/lGXQyJ4ZOkQazklSQo7cV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeoOG8%2FdJMcab4mPdg%2FlGXQyJ4ZOkQazklSQo7cV1%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;550&quot; height=&quot;300&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 11월, 같이 CS 스터디를 하던 스터디원이 채널톡 면접을 보고 왔다는 얘기를 듣고 공고를 찾아봤다. 코딩 테스트 화면 우하단에 매번 보이던 회사라이름은 익숙했는데, 마침 지원 자격도 맞아서 갖고 있던 이력서를 그냥 넣었다. 이전까지 계속 서류에서 떨어지던 경험이 있어서, 이번에도 비슷하게 끝나겠지 싶었다. 그런데 운 좋게 서류를 통과했고, 전형은 서류부터 3차 인터뷰까지 총 5단계였는데 2차에서 탈락했다. 이 글에서는 각 단계를 어떻게 준비했고, 실제로 어떻게 느꼈는지를 정리해두려 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-01 181906.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhigaT/dJMcaflo9mE/alasjqcTxezrnyYTcPA730/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhigaT/dJMcaflo9mE/alasjqcTxezrnyYTcPA730/img.png&quot; data-alt=&quot;채용 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhigaT/dJMcaflo9mE/alasjqcTxezrnyYTcPA730/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhigaT%2FdJMcaflo9mE%2FalasjqcTxezrnyYTcPA730%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;550&quot; height=&quot;318&quot; data-filename=&quot;스크린샷 2026-03-01 181906.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;742&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 서류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;25년&amp;nbsp;4월에&amp;nbsp;써뒀던&amp;nbsp;이력서와&amp;nbsp;포트폴리오를&amp;nbsp;그냥&amp;nbsp;제출했다.&amp;nbsp;4월&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;그럼에도&amp;nbsp;2차까지 올라간 건 순전히 코딩 덕분이 아닐까라는 생각을 한다. 그렇다고 코딩을 그렇게 잘하는 것도 아니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 사전 인터뷰 : 긴장 속 라이브 코딩&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11월&amp;nbsp;20일쯤&amp;nbsp;지원서를&amp;nbsp;넣었는데,&amp;nbsp;넣었다는&amp;nbsp;사실을&amp;nbsp;까먹을&amp;nbsp;즈음&amp;nbsp;모르는&amp;nbsp;010&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;/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.1 준비&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잡플래닛으로&amp;nbsp;미리&amp;nbsp;조사했더니&amp;nbsp;25분&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;LRU&amp;nbsp;캐시까지&amp;nbsp;`LinkedHashMap`으로&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;/p&gt;
&lt;figure id=&quot;og_1772442450643&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;Blind 75 - LeetCode&quot; data-og-description=&quot;Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.&quot; data-og-host=&quot;leetcode.com&quot; data-og-source-url=&quot;https://leetcode.com/problem-list/oizxjoit/)&quot; data-og-url=&quot;https://leetcode.com/problem-list/oizxjoit/)/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c3PNmW/dJMb81fP7yK/wMU8SjeN6OnxeNe4yH6Wlk/img.png?width=500&amp;amp;height=260&amp;amp;face=0_0_500_260&quot;&gt;&lt;a href=&quot;https://leetcode.com/problem-list/oizxjoit/)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leetcode.com/problem-list/oizxjoit/)&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c3PNmW/dJMb81fP7yK/wMU8SjeN6OnxeNe4yH6Wlk/img.png?width=500&amp;amp;height=260&amp;amp;face=0_0_500_260');&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;Blind 75 - LeetCode&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leetcode.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 당일 체감&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;손이&amp;nbsp;따라오기&amp;nbsp;시작했다. &lt;br /&gt;&lt;br /&gt;요구사항은 전부 만족했는데, 면접관분께서 테스트 코드를 돌려보니 버그가 2개 나왔다. 둘 다 그 자리에서 바로 잡을 수 있는 수준이었다. 마지막에 설계 의도 질문이 하나 나왔는데, 급하게 구현하다 보니 제대로 답을 못 했다. 면접이 끝나고도 그 부분이 계속 걸렸다. &lt;br /&gt;&lt;br /&gt;되게&amp;nbsp;걱정하고&amp;nbsp;있었는데,&amp;nbsp;다음날&amp;nbsp;오후에&amp;nbsp;합격&amp;nbsp;연락이&amp;nbsp;왔다.&amp;nbsp;사전인터뷰&amp;nbsp;때처럼&amp;nbsp;비슷하게&amp;nbsp;1차&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 1차 인터뷰 : 예상 밖의 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전인터뷰와&amp;nbsp;달리&amp;nbsp;1차는&amp;nbsp;사전&amp;nbsp;정보가&amp;nbsp;거의&amp;nbsp;없었다.&amp;nbsp;잡플래닛&amp;nbsp;후기도&amp;nbsp;추상적인&amp;nbsp;것들뿐이라&amp;nbsp;결국&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;/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.1 준비&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PS 위주라는 얘기를 듣고 리트코드 문제를 20개 정도 풀었고, LRU, LFU 캐시도 풀어봤다. CS도 살짝 복습했는데, 솔직히 이렇게 준비한 게 실전에서 크게 도움이 되진 않았다. CS는 아래 링크정도만 몇 번 읽어보았다.&lt;br /&gt;&lt;br /&gt;CS 참고:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev-coco.tistory.com/158&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev-coco.tistory.com/158&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/4z7l/tech_interview.zip/tree/main&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/4z7l/tech_interview.zip/tree/main&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gyoogle.dev/blog/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://gyoogle.dev/blog&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 당일 체감&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 처음 봤을 때 눈앞이 깜깜했다. 특정 기능을 직접 구현하는 문제였는데, 한 번도 해본 적이 없었다. 일단 그때를 다시 생각해 보면 급하게 머릿속에 떠오르는 방법으로 구현 계획을 간단히 이야기하고 시작했던 것 같다. &lt;br /&gt;&lt;br /&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;읽을&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;수준이&amp;nbsp;됐다. &lt;br /&gt;&lt;br /&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;방향으로&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;구현&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;면접 스타일을 복기해보면 CS도 중요하지만 구현 자체에 더 무게가 실려 있었고, 내가 주로 쓰는 언어에 대한 지식과 문제에 접근하는&amp;nbsp;과정을&amp;nbsp;중요하게&amp;nbsp;보는&amp;nbsp;느낌이었다. &lt;br /&gt;&lt;br /&gt;합격하고도&amp;nbsp;의아했지만,&amp;nbsp;그래도&amp;nbsp;나름&amp;nbsp;잘&amp;nbsp;해냈다는&amp;nbsp;생각이&amp;nbsp;들어&amp;nbsp;기분이&amp;nbsp;좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 2차 인터뷰 : 처음&amp;nbsp;받아본&amp;nbsp;제대로&amp;nbsp;된&amp;nbsp;기술&amp;nbsp;면접&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차는&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;1차보다&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;수&amp;nbsp;있었던&amp;nbsp;면접이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 준비&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일로&amp;nbsp;CS,&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;CS는&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;CS&amp;nbsp;모의면접도&amp;nbsp;진행했다. &lt;br /&gt;&lt;br /&gt;CS&amp;nbsp;관련&amp;nbsp;자료 &lt;br /&gt;-&amp;nbsp;&lt;a href=&quot;https://mangkyu.tistory.com/88)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mangkyu.tistory.com/88)&lt;/a&gt;&lt;br /&gt;-&amp;nbsp;&lt;a href=&quot;https://incheol-jung.gitbook.io/docs/q-and-a/spring&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://incheol-jung.gitbook.io/docs/q-and-a/spring&lt;/a&gt;&lt;br /&gt;-&amp;nbsp;&lt;a href=&quot;https://dev-coco.tistory.com/163&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev-coco.tistory.com/163&lt;/a&gt;&lt;br /&gt;-&amp;nbsp;&lt;a href=&quot;https://github.com/gyoogle/tech-interview-for-developer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/gyoogle/tech-interview-for-developer&lt;/a&gt;&lt;br /&gt;-&amp;nbsp;&lt;a href=&quot;https://github.com/ksundong/backend-interview-question&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/ksundong/backend-interview-question&lt;/a&gt;&lt;br /&gt;-&amp;nbsp;&lt;a href=&quot;https://github.com/jbee37142/Interview_Question_for_Beginner&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/jbee37142/Interview_Question_for_Beginner&lt;/a&gt;&lt;br /&gt;-&amp;nbsp;&lt;a href=&quot;https://jojoldu.tistory.com/784&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jojoldu.tistory.com/784&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;이력서&amp;nbsp;기반&amp;nbsp;질문도&amp;nbsp;친구에게&amp;nbsp;부탁해&amp;nbsp;모의면접을&amp;nbsp;한&amp;nbsp;번&amp;nbsp;했다.&amp;nbsp;다만&amp;nbsp;CS에&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;이&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;컬처핏은&amp;nbsp;채널톡&amp;nbsp;기술&amp;nbsp;블로그를&amp;nbsp;전부&amp;nbsp;읽고&amp;nbsp;유튜브&amp;nbsp;테크&amp;nbsp;컨퍼런스&amp;nbsp;영상까지&amp;nbsp;챙겨봤다.&amp;nbsp;그중&amp;nbsp;Redis&amp;nbsp;Pub/Sub을&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;/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;4.2 당일 체감&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총&amp;nbsp;80분,&amp;nbsp;1시간&amp;nbsp;기술&amp;nbsp;+&amp;nbsp;20분&amp;nbsp;컬처핏이었다.&amp;nbsp;CS는&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;&quot;내가&amp;nbsp;했던&amp;nbsp;내용&amp;nbsp;정리하면&amp;nbsp;되겠지&quot;라고&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;결론적으로&amp;nbsp;2차&amp;nbsp;면접에서&amp;nbsp;탈락했다.&amp;nbsp;3차를&amp;nbsp;앞두고&amp;nbsp;2차에서&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음으로 제대로 된 기술 면접을 경험하며 내 수준을 객관적으로 돌아볼 수 있는 시간이었다. 이력서를 다시 쓰는 방향성을 잡게 된 계기가 됐고, CS를 각 잡고 다시 공부하지 않아도 되겠다는 자신감도 생겼다. 채널톡 블로그에서 읽은 내용이 이후 프로젝트 고도화로 이어진 것도 나름 수확이었다. 결국 실패했지만, 지금까지 본 면접 중 가장 많이 배운 전형이었다. 좋은 면접은 내 부족한 점을 보여주고 방향을 잡아주는 나침반 같다는 생각이 들었다.&lt;/p&gt;</description>
      <category>취준</category>
      <category>기술면접</category>
      <category>라이브 코딩테스트</category>
      <category>면접회고</category>
      <category>채널톡</category>
      <category>취준일기</category>
      <category>탈락회고</category>
      <author>chan_2</author>
      <guid isPermaLink="true">https://hechan2.tistory.com/34</guid>
      <comments>https://hechan2.tistory.com/34#entry34comment</comments>
      <pubDate>Mon, 2 Mar 2026 21:51:33 +0900</pubDate>
    </item>
    <item>
      <title>진행한 프로젝트 메인 페이지 조회 구조 및 성능 개선하기</title>
      <link>https://hechan2.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2024년부터 진행해온 축제 SaaS 서비스 프로젝트는 고객(축제 운영팀)마다 하나의 축제를 등록하고, 각 축제의 메인페이지를 위젯 단위로 구성해 축제 분위기에 맞는 사이트를 만들어주는 서비스입니다.&lt;br /&gt;&lt;br /&gt;2024년 상반기, 메트릭 모니터링을 통해 `/home/{festivalId}` API의 응답 시간이 다른 API들보다 눈에 띄게 높다는 것을 발견했습니다. 이를 단일 테이블 상속 전략으로 개선했었는데, 이 글에서는 그 당시 왜 그런 선택을 했고, 그 선택이 어떤 새로운 문제를 낳았으며, 이를 어떻게 해결했는지를 다룰려고 합니다. 나아가 2025년 한 해 동안의 &lt;b&gt;Google Analytics 통계&lt;/b&gt;를 기반으로 메인페이지 조회 성능을 한 단계 더 개선한 과정까지 함께 정리해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 단일 테이블 상속 전략 도입 배경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 메인페이지에 보여지는 여러 위젯들을 타입별로 테이블을 분리하고 있었다. 문제는 메인페이지 API였다. 메인페이지를 한 번 조회하면 Festival과 함께 5종류 위젯을 모두 가져와야 하는데, 각 위젯 테이블이 Festival과 일대다 관계로 연결되어 있으니 N+1 문제가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;397&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pj5GO/dJMcadVe2i3/kQbRAMM8efozxoKKwhfEk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pj5GO/dJMcadVe2i3/kQbRAMM8efozxoKKwhfEk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pj5GO/dJMcadVe2i3/kQbRAMM8efozxoKKwhfEk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpj5GO%2FdJMcadVe2i3%2FkQbRAMM8efozxoKKwhfEk0%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;555&quot; height=&quot;255&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;397&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;자연스럽게 Fetch JOIN으로 해결하려 했지만, 여기서 또 다른 벽에 부딪혔다. 여러 일대다 관계를 동시에 Fetch JOIN하면 카테시안 곱(Cartesian Product) 이 발생한다. Hibernate는 이를 `MultipleBagFetchException`으로 차단하고, 설령 `Set`으로 우회하더라도 결과 행이 폭발적으로 늘어나 성능이 오히려 악화된다. &lt;br /&gt;&lt;br /&gt;결국 다른 접근이 필요했고, 5개 타입이 공통 필드(`name`, `url`, `festival_id`)를 공유한다는 점에 착안하여 &lt;b&gt;단일 테이블 상속 전략&lt;/b&gt;을 도입했다. 하나의 테이블로 합치면 Fetch JOIN 하나로 모든 위젯을 가져올 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1771306857352&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Widget 통합 및 메인페이지 조회로직 개선 by HeeChanN &amp;middot; Pull Request #101 &amp;middot; festimap-org/waba-backend&quot; data-og-description=&quot;#️⃣연관된 이슈 ex) #100  작업 내용 문제 상황 2024년 10월 경희대 대학 축제에서는 축제 기간에 메인 페이지 접속 시 페이지 로딩이 느려지는 것을 경험했으며, 이를 모니터링하기위해 prometheus&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/festimap-org/waba-backend/pull/101&quot; data-og-url=&quot;https://github.com/festimap-org/waba-backend/pull/101&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/festimap-org/waba-backend/pull/101&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/festimap-org/waba-backend/pull/101&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;Widget 통합 및 메인페이지 조회로직 개선 by HeeChanN &amp;middot; Pull Request #101 &amp;middot; festimap-org/waba-backend&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;#️⃣연관된 이슈 ex) #100  작업 내용 문제 상황 2024년 10월 경희대 대학 축제에서는 축제 기간에 메인 페이지 접속 시 페이지 로딩이 느려지는 것을 경험했으며, 이를 모니터링하기위해 prometheus&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌이켜 보면, 당시에는 단일 테이블 상속 전략의 장단점을 충분히 따져보지 않았다. 솔직히 그때는 개발 경험이 부족했고, 문제를 바라보는 시야가 단편적이었다. &lt;b&gt;&quot;N+1이 발생한다 &amp;rarr; 테이블을 합치면 된다 &amp;rarr; STI가 있네 &amp;rarr; 적용하자&quot;&lt;/b&gt; 식의 일직선적인 사고 흐름이었다. 당장 눈앞의 문제(N+1)가 해결되니까 또 테스트를 해봤을 때 성능적으로도 개선됬으니까 그걸로 충분하다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 역시, 초기에는 잘 동작했지만, 서비스가 새로운 축제에 도입되며 단일 테이블 상속 전략의 &lt;b&gt;구조적 한계&lt;/b&gt;가 하나둘 드러나기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 단일 테이블 상속 전략의 문제점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. NULL 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 문제는 도입하면서부터 알 수 있는 &lt;b&gt;NULL 컬럼 폭발문제&lt;/b&gt;이다. 하나의 테이블에 여러 type의 위젯이 존재하기 때문에 각 위젯마다 필요한 컬럼이 존재했고 그에따라 아래와 같이 null을 갖게 되는 경우가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJX0bb/dJMcabJXBCX/wqGOtsu6ACGgHSoXZdokjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJX0bb/dJMcabJXBCX/wqGOtsu6ACGgHSoXZdokjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJX0bb/dJMcabJXBCX/wqGOtsu6ACGgHSoXZdokjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJX0bb%2FdJMcabJXBCX%2FwqGOtsu6ACGgHSoXZdokjK%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;555&quot; height=&quot;165&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;모든 행에서 자기 타입과 무관한 컬럼들이 NULL로 채워진다. 위젯 타입이 5개뿐인 지금도 이 정도인데, 타입이 늘어날수록 NULL 비율은 가속도로 증가한다는 문제가 존재했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. 스키마 경직성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 문제는 &lt;b&gt;스키마 경직성&lt;/b&gt;이다. 이 문제는 프로젝트가 추구하는 &lt;b&gt;SaaS 서비스에서 가장 치명적인 문제&lt;/b&gt;이다. 축제마다 필요한 위젯이 다르다. 어떤 축제는 상단 위젯이 필요하고, 어떤 축제는 중간 배너가 필요할 수 있다. 단일 테이블 상속 전략에서 새 위젯 타입을 추가하면 어떻게 될까? &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771307606045&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 타이머 위젯 추가
ALTER TABLE base_widget ADD COLUMN timer_duration INT;
ALTER TABLE base_widget ADD COLUMN timer_label VARCHAR(255);

-- 투표 위젯 추가
ALTER TABLE base_widget ADD COLUMN vote_options TEXT;
ALTER TABLE base_widget ADD COLUMN vote_deadline DATETIME;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;새 타입 하나를 추가할 때마다 `ALTER TABLE`이 필요하다. 운영 중인 테이블에 DDL을 실행하는 것 자체가 리스크이며, 추가된 컬럼은 기존의 모든 위젯 행에 NULL로 들어간다. 위젯 타입이 10개, 20개로 늘어난다면? 테이블은 거대한 NULL 매트릭스가 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-3. 도메인 제약 불가능&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입별로 필수인 컬럼이 달라도 DB 레벨에서 NOT NULL 제약을 걸 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1771307817052&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- SquareWidget은 description이 필수지만...
ALTER TABLE base_widget ALTER COLUMN description SET NOT NULL;
-- &amp;rarr; MainWidget도 description을 쓰지만, UpWidget/DownWidget은 쓰지 않으므로 불가!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국&amp;nbsp;모든&amp;nbsp;제약은&amp;nbsp;애플리케이션&amp;nbsp;레벨로&amp;nbsp;올라가야&amp;nbsp;한다.&amp;nbsp;DB가&amp;nbsp;데이터&amp;nbsp;무결성을&amp;nbsp;보장해주지&amp;nbsp;못하면&amp;nbsp;어떻게&amp;nbsp;될까? &lt;br /&gt;&lt;br /&gt;서비스&amp;nbsp;코드&amp;nbsp;곳곳에&amp;nbsp;유효성&amp;nbsp;검증&amp;nbsp;로직이&amp;nbsp;흩어진다.&amp;nbsp;SquareWidget을&amp;nbsp;생성하는&amp;nbsp;Service에서&amp;nbsp;`description`이&amp;nbsp;비어&amp;nbsp;있는지&amp;nbsp;직접&amp;nbsp;검사해야&amp;nbsp;하고,&amp;nbsp;MainWidget을&amp;nbsp;생성하는&amp;nbsp;Service에서도&amp;nbsp;또&amp;nbsp;검사해야&amp;nbsp;한다.&amp;nbsp;타입이&amp;nbsp;늘어날수록&amp;nbsp;&quot;이&amp;nbsp;타입에서&amp;nbsp;이&amp;nbsp;필드는&amp;nbsp;필수인가?&quot;를&amp;nbsp;코드로&amp;nbsp;일일이&amp;nbsp;관리해야&amp;nbsp;하며,&amp;nbsp;이&amp;nbsp;검증을&amp;nbsp;누락하면&amp;nbsp;NULL이&amp;nbsp;그대로&amp;nbsp;저장되어도&amp;nbsp;DB는&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;DB가&amp;nbsp;마지막&amp;nbsp;방어선&amp;nbsp;역할을&amp;nbsp;하지&amp;nbsp;못하는&amp;nbsp;구조인&amp;nbsp;셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 메인 페이지 조회 구조 변경 : JSON 컬럼 도입&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-1. 왜 JSON 컬럼이 SaaS 메인페이지에 적합한가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 테이블 상속 전략의 문제를 인식한 뒤 여러 대안을 검토했다. 결론은 MySQL의 &lt;b&gt;JSON 컬럼&lt;/b&gt;이었다. 그중 가장 큰 이유는 &lt;b&gt;스키마 유연성&lt;/b&gt;이었다. &lt;br /&gt;&lt;br /&gt;축제&amp;nbsp;SaaS&amp;nbsp;서비스에서는&amp;nbsp;고객별로&amp;nbsp;필요한&amp;nbsp;위젯이&amp;nbsp;다르다.&amp;nbsp;JSON&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;DDL&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;b&gt;위젯 속성이 조회 위주&lt;/b&gt;라는 점이었다. 위젯의 타입별 속성(`image`, `description`, `periodStart` 등)은 생성 시 저장하고, 이후에는 읽기만 한다. 복잡한 업데이트 쿼리가 필요 없으므로 JSON의 쓰기 성능 단점이 거의 드러나지 않는다. &lt;br /&gt;&lt;br /&gt;마지막으로 &lt;b&gt;JSON 컬럼의 단점이 드러나지 않는 구조&lt;/b&gt;라는 점이었다. 타입별 속성으로 복잡한 쿼리(`JOIN`, `GROUP BY`, 정렬 등)가 필요하면 `JSON_EXTRACT`를 사용해야 하는데, 성능이 일반 컬럼보다 떨어진다. 하지만 현재 동작에서 위젯 속성으로 복잡한 WHERE/JOIN이 불필요했다. 위젯 조회는 `festival_id`와 `widget_type`으로 필터링하면 끝이다. 타입별 속성을 기준으로 `JOIN`하거나 `GROUP BY`할 일이 없으므로, JSON의 쿼리 약점이 부각되지 않는다. &lt;br /&gt;&lt;br /&gt;물론&amp;nbsp;데이터&amp;nbsp;무결성&amp;nbsp;검증을&amp;nbsp;DB가&amp;nbsp;아닌&amp;nbsp;애플리케이션에서&amp;nbsp;해야&amp;nbsp;한다는&amp;nbsp;것은&amp;nbsp;여전히&amp;nbsp;남겨진&amp;nbsp;문제다.&amp;nbsp;이&amp;nbsp;부분은&amp;nbsp;Properties&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;DB&amp;nbsp;제약만큼&amp;nbsp;강력하지는&amp;nbsp;않지만,&amp;nbsp;JSON의&amp;nbsp;타입별&amp;nbsp;속성이&amp;nbsp;반드시&amp;nbsp;Properties&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;이러한&amp;nbsp;단점에도&amp;nbsp;불구하고,&amp;nbsp;메인페이지&amp;nbsp;구성&amp;nbsp;정보를&amp;nbsp;&lt;b&gt;축제별로&amp;nbsp;유연하게&amp;nbsp;관리하는&amp;nbsp;용도라면&amp;nbsp;스키마&amp;nbsp;유연성의&amp;nbsp;이점이&amp;nbsp;훨씬&amp;nbsp;크다&lt;/b&gt;고&amp;nbsp;판단하여&amp;nbsp;&lt;b&gt;JSON&amp;nbsp;컬럼&amp;nbsp;도입&lt;/b&gt;을&amp;nbsp;결정하게&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;1052&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bytsd9/dJMb996qxMj/66XEgpJEgUyfcuH9B6Lks1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bytsd9/dJMb996qxMj/66XEgpJEgUyfcuH9B6Lks1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bytsd9/dJMb996qxMj/66XEgpJEgUyfcuH9B6Lks1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbytsd9%2FdJMb996qxMj%2F66XEgpJEgUyfcuH9B6Lks1%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;555&quot; height=&quot;240&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-2. 왜 NoSQL(MongoDB)이 아닌 JSON 컬럼인가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;스키마 유연성이 필요하다면 MongoDB를 쓰는 게 낫지 않나?&quot; 라는 질문이 나올 수 있다. 처음에는 당연히 NoSQL 데이터베이스를 도입해야하나 고민했지만, 아래와 같은 이유로 선택을 하지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nBFU9/dJMcab4d7Mf/ElHQWjCkSAbpuQHiTbjjCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nBFU9/dJMcab4d7Mf/ElHQWjCkSAbpuQHiTbjjCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nBFU9/dJMcab4d7Mf/ElHQWjCkSAbpuQHiTbjjCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnBFU9%2FdJMcab4d7Mf%2FElHQWjCkSAbpuQHiTbjjCK%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;555&quot; height=&quot;207&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;또한 WidgetItem과의 FK 관계도 한목 하였다. 위젯은 하위에 `WidgetItem`이라는 자식 엔티티를 갖고 있고, 이 관계는 FK로 참조 무결성을 보장한다. MongoDB로 위젯을 옮기면 이 FK가 깨지고, 두 데이터스토어 사이의 정합성을 직접 관리해야 한다. 분산 트랜잭션이라는 거대한 복잡도를 떠안을 이유가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 결론은 유연해야 할 부분(타입별 속성)만 JSON으로 빼고, 나머지(공통 필드, 관계)는 RDB의 강점을 그대로 유지하는 것이 이 상황에서 가장 합리적인 선택이었다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 캐시 도입 배경 : GA 통계가 보여준 것&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZhqGO/dJMcaaRPHXo/IyxHNXNQDJTqjG1LQ6nmDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZhqGO/dJMcaaRPHXo/IyxHNXNQDJTqjG1LQ6nmDk/img.png&quot; data-alt=&quot;2025년 PV&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZhqGO/dJMcaaRPHXo/IyxHNXNQDJTqjG1LQ6nmDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZhqGO%2FdJMcaaRPHXo%2FIyxHNXNQDJTqjG1LQ6nmDk%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;542&quot; height=&quot;170&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2025년 PV&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;JSON 컬럼 도입으로 &lt;b&gt;스키마 유연성&lt;/b&gt;&amp;nbsp;문제는 해결했다. 새 위젯 타입을 추가할 때 더 이상 운영 DB를 건드릴 필요가 없고, NULL 컬럼 폭발도 사라졌다. 그렇다면 메인페이지 API 성능은 어떨까? &lt;br /&gt;&lt;br /&gt;2025년&amp;nbsp;한&amp;nbsp;해&amp;nbsp;동안의&amp;nbsp;&lt;b&gt;Google&amp;nbsp;Analytics&amp;nbsp;통계&lt;/b&gt;를&amp;nbsp;확인해보니,&amp;nbsp;총&amp;nbsp;51만&amp;nbsp;PV&amp;nbsp;중&amp;nbsp;메인&amp;nbsp;홈(`/`)이&amp;nbsp;31.7만(62%)을&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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;극단적인 Read-Heavy: 관리자가 위젯을 수정할 때만 데이터가 변경된다. 하루 수 회 수준 (거의 없음)&lt;/li&gt;
&lt;li&gt;스파이크 트래픽: 축제 기간에 동일 데이터에 DB 쿼리 수천 건/분이 반복된다.&lt;/li&gt;
&lt;li&gt;이미 쿼리 최적화 완료: JSON 마이그레이션 + JOIN FETCH + 복합 인덱스로 단건 쿼리 자체는 최적화된 상태. 병목은 쿼리 자체가 아니라 쿼리 횟수다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;극단적인 Read:Write 비율 + 짧은 지연 허용(관리자 수정 후 수 초 내 반영이면 충분) &amp;rarr; 캐시 도입의 최적 조건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 홈 데이터의 경우 변경 빈도가 낮아 데이터 최신성 부담이 거의 없고, 수 초 수준의 지연만 허용되면 되므로 캐시 무효화 전략도 단순하게 가져갈 수 있다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n7BlH/dJMcaajYZbr/WcaH0S8cBkFvrPdtnxwnn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n7BlH/dJMcaajYZbr/WcaH0S8cBkFvrPdtnxwnn1/img.png&quot; data-alt=&quot;초기 인프라 구조 (No Redis)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n7BlH/dJMcaajYZbr/WcaH0S8cBkFvrPdtnxwnn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn7BlH%2FdJMcaajYZbr%2FWcaH0S8cBkFvrPdtnxwnn1%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;980&quot; height=&quot;649&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;초기 인프라 구조 (No Redis)&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;현재 구조는 위와 같다. 클라이언트의 모든 요청이 ALB를 거쳐 EC2 인스턴스에 도달하고, 인스턴스는 &lt;b&gt;매 요청마다 MySQL에 직접 쿼리&lt;/b&gt;를 날린다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 첫 번째 시도 : 글로벌 캐시 도입 (Redis, ElastiCache Valkey)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Festimap은 다중 인스턴스 구조로 운영되고 있다. ALB 뒤에 Auto Scaling 그룹이 구성되어 있어, 축제 기간에 트래픽이 몰리면 인스턴스가 자동으로 늘어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, 각 인스턴스가 독립적으로 로컬 캐시를 들고 있으면, 인스턴스 간 데이터 정합성을 보장할 수 없다. 인스턴스 A에서 관리자가 위젯을 수정해도 인스턴스 B의 캐시에는 여전히 이전 데이터가 남아 있기 때문이다. 따라서 첫 번째 단계로는 모든 인스턴스가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하나의 외부 캐시를 공유&lt;/b&gt;하는 글로벌 캐시 방식을 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5-1. Self-hosted vs AWS Managed Service&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 직접 EC2에 올려 운영하는 방법도 있지만, 현재 프로젝트에서 Redis 서버의 모니터링, 보안 패치, 장애 대응까지 직접 관리하는 것은 현실적이지 않았다. 따라서, AWS ElastiCache를 선택하면 이런 관리 오버헤드를 제거하고 개발에 집중할 수 있다고 판단했다. 또한 직접 Redis 환경을 구축했을 때 운영 비용은 월 $12의 ElastiCache 요금보다 훨씬 크다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5-2. Redis vs Valkey&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년 3월, Redis Labs가 라이선스를 SSPL에서 RSAL로 변경하면서 클라우드 벤더들의 Redis 호스팅이 제한되었다. 이에 AWS는 Redis의 오픈소스 포크인 &lt;b&gt;Valkey&lt;/b&gt;를 ElastiCache의 기본 엔진으로 권장하기 시작했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boOAlT/dJMcahKbLCy/ip5gEBoy9Lkpdtgb6X1Oz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boOAlT/dJMcahKbLCy/ip5gEBoy9Lkpdtgb6X1Oz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boOAlT/dJMcahKbLCy/ip5gEBoy9Lkpdtgb6X1Oz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboOAlT%2FdJMcahKbLCy%2Fip5gEBoy9Lkpdtgb6X1Oz0%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;338&quot; height=&quot;162&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 스펙에서 Valkey가 Redis보다 &lt;b&gt;약 20% 저렴&lt;/b&gt;하다. 가장 저렴한 조합은 Graviton 기반 `cache.t4g.micro` + Valkey로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월 $14.02다. 여기에 1년 예약 인스턴스를 적용하면 시간당 $0.013까지 내려가 &lt;b&gt;월 ~$9.5&lt;/b&gt; 수준이 된다. 참고로 AWS 프리 티어를 활용하면 `cache.t3.micro` 기준 &lt;b&gt;첫 12개월은 무료&lt;/b&gt;다 (말도 안돼~ 프리티어 최고!).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;`cache.t3.micro`를 선택해 시작했다. 현재로서는 프리티어 사양으로도 충분히 동작한다고 생각하였다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5-3. 적용방식&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의&amp;nbsp;`@Cacheable`을&amp;nbsp;활용하면&amp;nbsp;기존&amp;nbsp;서비스&amp;nbsp;코드를&amp;nbsp;거의&amp;nbsp;건드리지&amp;nbsp;않고&amp;nbsp;캐시를&amp;nbsp;적용할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771509373517&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Cacheable(value = &quot;home&quot;, key = &quot;#festivalId&quot;)
public HomeDto getMainPage(Long festivalId) {
    // Festival + Widget JOIN FETCH + 공지 + 실종자 조회
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 무효화는 &lt;b&gt;도메인 이벤트 &lt;/b&gt;기반으로 설계했다. 위젯, 공지, 실종자 서비스의 mutation 메서드에서 `HomeCacheEvictEvent`를 발행하고, `@TransactionalEventListener(phase = AFTER_COMMIT)`으로 트랜잭션 커밋 후에만 evict를 수행한다. 트랜잭션이 롤백되면 캐시를 무효화할 필요가 없기 때문이다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;TTL은&amp;nbsp;5분으로&amp;nbsp;설정했다&lt;/b&gt;. 이 값은 다음 두 가지 기준에서 도출했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1771513025156&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;Cache Validity - Database Caching Strategies Using Redis&quot; data-og-description=&quot;This whitepaper is for historical reference only. Some content might be outdated and some links might not be available. Cache Validity You can control the freshness of your cached data by applying a time to live (TTL) or expiration to your cached keys. Aft&quot; data-og-host=&quot;docs.aws.amazon.com&quot; data-og-source-url=&quot;https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/cache-validity.html&quot; data-og-url=&quot;https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/cache-validity.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/cache-validity.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/cache-validity.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Cache Validity - Database Caching Strategies Using Redis&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This whitepaper is for historical reference only. Some content might be outdated and some links might not be available. Cache Validity You can control the freshness of your cached data by applying a time to live (TTL) or expiration to your cached keys. Aft&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1771513037432&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;Cache optimization: Strategies to cut latency and cloud cost&quot; data-og-description=&quot;A practical guide to cache optimization: TTLs, eviction, cache-aside, write-through, edge caching, and AI/semantic caching to cut latency and cost.&quot; data-og-host=&quot;redis.io&quot; data-og-source-url=&quot;https://redis.io/blog/guide-to-cache-optimization-strategies/&quot; data-og-url=&quot;https://redis.io/blog/guide-to-cache-optimization-strategies/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dnmUfS/dJMb9cBEGf5/7wOtm2Td4alMgc6eI9rnC1/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/k0h5n/dJMb9jgtLiX/5iBBGhakE5hB5DXKvFh9qk/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628&quot;&gt;&lt;a href=&quot;https://redis.io/blog/guide-to-cache-optimization-strategies/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://redis.io/blog/guide-to-cache-optimization-strategies/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dnmUfS/dJMb9cBEGf5/7wOtm2Td4alMgc6eI9rnC1/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/k0h5n/dJMb9jgtLiX/5iBBGhakE5hB5DXKvFh9qk/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628');&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;Cache optimization: Strategies to cut latency and cloud cost&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A practical guide to cache optimization: TTLs, eviction, cache-aside, write-through, edge caching, and AI/semantic caching to cut latency and cost.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;redis.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL을 결정할 때 두 가지 질문을 던지라고 권장한다: &lt;b&gt;데이터가 얼마나 자주 변하는가&lt;/b&gt;, 그리고 &lt;b&gt;오래된 데이터를 반환했을 때의 위험은 무엇인가&lt;/b&gt;. Redis 공식 블로그에서도&amp;nbsp;데이터&amp;nbsp;변경&amp;nbsp;빈도에&amp;nbsp;따라&amp;nbsp;TTL&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;API&amp;nbsp;응답)는&amp;nbsp;5~60분을&amp;nbsp;권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메인홈&amp;nbsp;데이터의&amp;nbsp;변경&amp;nbsp;빈도는&amp;nbsp;하루&amp;nbsp;수&amp;nbsp;회&amp;nbsp;수준&lt;/b&gt;이다. 관리자가 데이터를 수정하면 도메인 이벤트가 즉시 캐시를 무효화하므로, TTL은 이벤트가 누락되었을 때를 대비한 &lt;b&gt;안전망 역할&lt;/b&gt;이다. Redis 공식 블로그의 기준에서 &lt;b&gt;가끔 변하는 데이터&lt;/b&gt;에 해당하는 5~60분 범위의 상한인 &lt;b&gt;TTL 5분&lt;/b&gt;으로 설정했다. 대부분의 갱신은 도메인 이벤트가 처리하고, TTL은 혹시 모를 상황에서 최대 5분 안에 자동 만료되는 보험이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1139&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXK7DF/dJMcagR2uj7/pz40kTBKqmE4zZssUIMSP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXK7DF/dJMcagR2uj7/pz40kTBKqmE4zZssUIMSP0/img.png&quot; data-alt=&quot;글로벌 캐시 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXK7DF/dJMcagR2uj7/pz40kTBKqmE4zZssUIMSP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXK7DF%2FdJMcagR2uj7%2Fpz40kTBKqmE4zZssUIMSP0%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;1139&quot; height=&quot;752&quot; data-origin-width=&quot;1139&quot; data-origin-height=&quot;752&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5-4. 글로벌 캐시의 구조적 한계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k6를&amp;nbsp;사용해&amp;nbsp;같은&amp;nbsp;VPC&amp;nbsp;내&amp;nbsp;EC2&amp;nbsp;Spot에서&amp;nbsp;1,000&amp;nbsp;VU&amp;nbsp;부하&amp;nbsp;테스트를&amp;nbsp;수행했다(ramping-vus,&amp;nbsp;0&amp;nbsp;&amp;rarr;&amp;nbsp;200&amp;nbsp;&amp;rarr;&amp;nbsp;1,000&amp;nbsp;&amp;rarr;&amp;nbsp;0,&amp;nbsp;약&amp;nbsp;5.5분).&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccZ7iU/dJMcacB6kOy/XxC1XQnsQNRlfLWr4rQJb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccZ7iU/dJMcacB6kOy/XxC1XQnsQNRlfLWr4rQJb1/img.png&quot; data-alt=&quot;글로벌 캐시 도입 효과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccZ7iU/dJMcacB6kOy/XxC1XQnsQNRlfLWr4rQJb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccZ7iU%2FdJMcacB6kOy%2FXxC1XQnsQNRlfLWr4rQJb1%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;152&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;132&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;Redis 캐시 도입만으로 p95가 434ms로 떨어졌다. Throughput도 2배 이상 증가했다. DB 직접 쿼리 대비 확실한 개선이라고 볼 수 있다.&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;908&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czT41j/dJMcacB6isN/rrAR1s01u9ZzKWpFfUbKEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czT41j/dJMcacB6isN/rrAR1s01u9ZzKWpFfUbKEK/img.png&quot; data-alt=&quot;기존 인프라 테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czT41j/dJMcacB6isN/rrAR1s01u9ZzKWpFfUbKEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczT41j%2FdJMcacB6isN%2FrrAR1s01u9ZzKWpFfUbKEK%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;908&quot; height=&quot;311&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기존 인프라 테스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtWSVF/dJMcaihZ3EW/oxkKllkg7kdhZWAGDVSKlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtWSVF/dJMcaihZ3EW/oxkKllkg7kdhZWAGDVSKlK/img.png&quot; data-alt=&quot;글로벌 캐시 도입&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtWSVF/dJMcaihZ3EW/oxkKllkg7kdhZWAGDVSKlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtWSVF%2FdJMcaihZ3EW%2FoxkKllkg7kdhZWAGDVSKlK%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;922&quot; height=&quot;309&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;309&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;하지만 CloudWatch 메트릭을 확인해보니, 이 구조가 갖는 한계도 함께 드러났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XdI0c/dJMcahpSdnk/7aeV8YZB3BPlgEd5lhtEAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XdI0c/dJMcahpSdnk/7aeV8YZB3BPlgEd5lhtEAK/img.png&quot; data-alt=&quot;CloudWatch 로그&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XdI0c/dJMcahpSdnk/7aeV8YZB3BPlgEd5lhtEAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXdI0c%2FdJMcahpSdnk%2F7aeV8YZB3BPlgEd5lhtEAK%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;664&quot; height=&quot;222&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CloudWatch 로그&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 히트여도 &lt;b&gt;매 요청마다 Redis로 네트워크 왕복&lt;/b&gt;이 발생하고, 응답 JSON을 &lt;b&gt;매번 역직렬화&lt;/b&gt;해야 한다. 인스턴스가 늘어나면 그만큼 네트워크 트래픽이 증가해 글로벌 캐시 자체가 병목으로 전환될 수 있는 수치였다.&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. Caffeine 로컬 캐시 + Redis Pub/Sub&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6-1. 채널톡 기술 블로그에서 얻은 인사이트&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 캐시의 한계를 확인한 시점에서, 채널 톡의 로컬 캐시 도입기&lt;a href=&quot;https://channel.io/ko/team/blog/articles/tech-distributed-cache-1-67a392c5&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://channel.io/ko/team/blog/articles/tech-distributed-cache-1-67a392c5&lt;/a&gt;를 읽게&amp;nbsp;되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 블로그의 핵심 아이디어는 &lt;b&gt;Redis를 캐시 저장소가 아닌 무효화 메시징 채널로만 사용&lt;/b&gt;하는 것이었다. 읽기 경로에서 네트워크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용을 완전히 제거하고, 캐시 무효화 신호만 Redis Pub/Sub으로 전파하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도&amp;nbsp;이&amp;nbsp;구조가&amp;nbsp;유리하다.&amp;nbsp;글로벌&amp;nbsp;캐시는&amp;nbsp;캐시&amp;nbsp;히트여도&amp;nbsp;&lt;b&gt;네트워크&amp;nbsp;왕복&amp;nbsp;+&amp;nbsp;JSON&amp;nbsp;역직렬화가&amp;nbsp;매&amp;nbsp;요청마다&amp;nbsp;발생&lt;/b&gt;한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 로컬 캐시는 같은 JVM 힙에 객체 참조로 들고 있으므로 조회 비용이 사실상 0이다. 대신 다중 인스턴스 환경에서 캐시 정합성이라는 새로운 문제가 생기는데, 이것을 Redis Pub/Sub이 해결한다. 읽기는 네트워크를 타지 않고, 쓰기(무효화)만 네트워크를 타는 구조로 메인홈처럼 읽기가 압도적으로 많은 워크로드에서는 이상적인 분리라고 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6-2. 설계&amp;nbsp;과정에서&amp;nbsp;고민한&amp;nbsp;것들&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널톡 블로그에서 배운 점과 Festimap 상황에 맞게 주의해서 결정한 것들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;키 무효화 vs 키+값 동기화&lt;/b&gt; : 무효화 시 키만 날리고, 다음 조회에서 DB 로드하는 방식을 선택했다. 새 값을 함께 전파하면 직렬화 비용이 추가되고, 두 인스턴스가 거의 동시에 수정하면 race condition이 발생할 수 있다. 키만 무효화하면 항상 DB에서 최신 데이터를 로드하므로 정합성이 보장된다. 관리자 변경이 하루 수 회 수준이니 이 DB 히트 비용은 무시할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 유실 대비 두 가지 안전망&lt;/b&gt; : Redis Pub/Sub은 At-most-once(최대 한번) 전달이라 메시지가 유실될 수 있다. 이를 보완하기 위해 두 가지 안전망을 설계했다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Caffeine TTL 1분&lt;/b&gt;: 메시지가 유실되어도 최대 1분 후 자동 만료된다. 캐시 무효화를&amp;nbsp; 포함한 부하 테스트에서 분당 12회 무효화를 수행해도 성능 차이가 거의 없었으므로 TTL을 짧게 잡아도 안전하다고 판단했다. 다만 5초 수준까지 줄이면 캐시 재생성이 너무 빈번해지므로, 메시지 유실 시에도 메인홈 데이터가 1분 정도 지연되는 것은 사용자 경험에 큰 영향이 없다는 점을 고려해 1분으로 설정했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재연결 시 전체 캐시 클리어&lt;/b&gt;: Redis 연결이 끊겼다가 복구되면, 끊긴 동안 놓친 메시지가 있을 수 있으므로 전체 캐시를 비운다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lU14R/dJMcacPDBaT/ObMbmkw1QhebisKCXyHE61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lU14R/dJMcacPDBaT/ObMbmkw1QhebisKCXyHE61/img.png&quot; data-alt=&quot;Pub/Sub + Caffeine 캐시 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lU14R/dJMcacPDBaT/ObMbmkw1QhebisKCXyHE61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlU14R%2FdJMcacPDBaT%2FObMbmkw1QhebisKCXyHE61%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;1062&quot; height=&quot;720&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pub/Sub + Caffeine 캐시 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서 변한 핵심 코드는 `DistributedCacheManager`다. Caffeine 캐시를 내부에 두고 다음과 같이 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;읽기 경로 (get)&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;Caffeine 히트 &amp;rarr; 바로 반환&lt;/li&gt;
&lt;li&gt;Caffeine 미스 &amp;rarr; DB 조회 &amp;rarr; Caffeine에 저장 &amp;rarr; 반환&lt;/li&gt;
&lt;li&gt;Redis Pub/Sub은 관여하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 무효화 경로 (invalidate)&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;/li&gt;
&lt;li&gt;로컬 Caffeine 무효화 + Redis Pub/Sub으로 다른 인스턴스에 무효화 신호 전파&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 부하테스트 비교 : 최종 결과&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트 환경&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;도구&lt;/b&gt;: k6 (ramping-vus, 0 &amp;rarr; 200 &amp;rarr; 1,000 &amp;rarr; 0, 약 5.5분)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실행 위치&lt;/b&gt;: 같은 VPC 내 EC2 Spot&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대상&lt;/b&gt;: ALB &amp;rarr; EC2 2대 &amp;rarr; MySQL + ElastiCache Valkey&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Read-Only 테스트&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rnsyU/dJMcabwpbvy/cnnkgnkjLZh3cbQ6WhGL1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rnsyU/dJMcabwpbvy/cnnkgnkjLZh3cbQ6WhGL1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rnsyU/dJMcabwpbvy/cnnkgnkjLZh3cbQ6WhGL1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrnsyU%2FdJMcabwpbvy%2FcnnkgnkjLZh3cbQ6WhGL1k%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;906&quot; height=&quot;323&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FJBw6/dJMcabiUkkH/jkiyicaK582syC7STLEDaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FJBw6/dJMcabiUkkH/jkiyicaK582syC7STLEDaK/img.png&quot; data-alt=&quot;Caffeine + Pub/Sub 도입&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FJBw6/dJMcabiUkkH/jkiyicaK582syC7STLEDaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFJBw6%2FdJMcabiUkkH%2FjkiyicaK582syC7STLEDaK%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;116&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Caffeine + Pub/Sub 도입&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Read/Write : 캐시 무효화 발생 테스트&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1,000 VU 읽기 트래픽과 분당 12회 관리자 캐시 무효화를 동시에 수행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W1kq0/dJMcacWnvji/jPMnoiNKztjxVq02Kwym7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W1kq0/dJMcacWnvji/jPMnoiNKztjxVq02Kwym7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W1kq0/dJMcacWnvji/jPMnoiNKztjxVq02Kwym7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW1kq0%2FdJMcacWnvji%2FjPMnoiNKztjxVq02Kwym7k%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;903&quot; height=&quot;317&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PJbQS/dJMcaa5lHIO/QPgg5mHLS0UpsDxUHnZAs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PJbQS/dJMcaa5lHIO/QPgg5mHLS0UpsDxUHnZAs0/img.png&quot; data-alt=&quot;캐시 무효화 테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PJbQS/dJMcaa5lHIO/QPgg5mHLS0UpsDxUHnZAs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPJbQS%2FdJMcaa5lHIO%2FQPgg5mHLS0UpsDxUHnZAs0%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;195&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;163&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;eviction&amp;nbsp;횟수가&amp;nbsp;거의&amp;nbsp;동일한&amp;nbsp;조건에서,&amp;nbsp;Throughput은&amp;nbsp;29%&amp;nbsp;증가하고&amp;nbsp;p95는&amp;nbsp;39%&amp;nbsp;감소했다.&amp;nbsp;무효화가&amp;nbsp;발생해도&amp;nbsp;성능&amp;nbsp;영향이&amp;nbsp;미미한&amp;nbsp;이유는,&amp;nbsp;eviction&amp;nbsp;직후&amp;nbsp;첫&amp;nbsp;요청만&amp;nbsp;DB로&amp;nbsp;가고&amp;nbsp;나머지는&amp;nbsp;곧바로&amp;nbsp;Caffeine&amp;nbsp;히트를&amp;nbsp;이어가기&amp;nbsp;때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CloudWatch Valkey 메트릭 비교 (&lt;/b&gt;&lt;b&gt;캐시 무효화 테스트)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zEtwY/dJMcagqXcbF/QK9rQoCAuGAkb32DueQZQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zEtwY/dJMcagqXcbF/QK9rQoCAuGAkb32DueQZQ1/img.png&quot; data-alt=&quot;글로벌 캐시 메트릭&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zEtwY/dJMcagqXcbF/QK9rQoCAuGAkb32DueQZQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzEtwY%2FdJMcagqXcbF%2FQK9rQoCAuGAkb32DueQZQ1%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;777&quot; height=&quot;269&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;글로벌 캐시 메트릭&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u9WvM/dJMcaaRP8g6/oGTdx3GRm7qlzWlv0YX6VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u9WvM/dJMcaaRP8g6/oGTdx3GRm7qlzWlv0YX6VK/img.png&quot; data-alt=&quot;Caffeine + Pub/Sub&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u9WvM/dJMcaaRP8g6/oGTdx3GRm7qlzWlv0YX6VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu9WvM%2FdJMcaaRP8g6%2FoGTdx3GRm7qlzWlv0YX6VK%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;774&quot; height=&quot;260&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Caffeine + Pub/Sub&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/munmN/dJMcaaqKwud/dRISLw9fiUA4GDn9TVbU80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/munmN/dJMcaaqKwud/dRISLw9fiUA4GDn9TVbU80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/munmN/dJMcaaqKwud/dRISLw9fiUA4GDn9TVbU80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmunmN%2FdJMcaaqKwud%2FdRISLw9fiUA4GDn9TVbU80%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;83&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;98&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BhRZC/dJMcaf6EpHT/oPDtAVvKksnKeEKFuTo1k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BhRZC/dJMcaf6EpHT/oPDtAVvKksnKeEKFuTo1k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BhRZC/dJMcaf6EpHT/oPDtAVvKksnKeEKFuTo1k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBhRZC%2FdJMcaf6EpHT%2FoPDtAVvKksnKeEKFuTo1k0%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;449&quot; height=&quot;133&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;133&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개선 작업을 진행하면서 두 가지 종류의 배움이 있었다. &lt;br /&gt;&lt;br /&gt;JSON 컬럼 도입은 직접 부딪혀야 알 수 있는 배움이었다. 단일 테이블 상속 전략의 한계를 글로만 읽었을 때는 와닿지 않았다. 실제로 NULL 컬럼이 늘어나는 것을 보고, 새 위젯 타입을 추가할 때마다 ALTER TABLE을 써야 하는 상황을 겪고 나서야 &lt;b&gt;이 구조는 SaaS에 맞지 않는다&lt;/b&gt;는 판단이 섰다. &lt;br /&gt;&lt;br /&gt;반면&lt;b&gt;&amp;nbsp;Caffeine&amp;nbsp;+&amp;nbsp;Pub/Sub&amp;nbsp;도입은&amp;nbsp;남의&amp;nbsp;경험에서&amp;nbsp;배운&amp;nbsp;것&lt;/b&gt;이었다. 글로벌 캐시의 CloudWatch 지표를 보며 &quot;&lt;b&gt;읽기 경로에서 네트워크를 제거해야 한다&lt;/b&gt;&quot;는 문제 인식까지는 도달했지만, 구체적으로 어떻게 해결할지는 채널톡 기술 블로그를 읽고 나서야 그림이 그려졌다. Redis를 캐시 저장소가 아닌 무효화 채널로만 쓴다는 발상, 키 무효화 vs 값 동기화의 트레이드오프, TTL과 Pub/Sub의 역할 분담같은 설계 판단들을 처음부터 혼자 도출하기는 어려웠을 것이다. &lt;br /&gt;&lt;br /&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;것을&amp;nbsp;경험한&amp;nbsp;시간이었다.&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>Caffeine</category>
      <category>elasticache</category>
      <category>JSON컬럼</category>
      <category>K6</category>
      <category>pub/sub</category>
      <category>Spring Boot</category>
      <author>chan_2</author>
      <guid isPermaLink="true">https://hechan2.tistory.com/33</guid>
      <comments>https://hechan2.tistory.com/33#entry33comment</comments>
      <pubDate>Fri, 20 Feb 2026 01:44:32 +0900</pubDate>
    </item>
    <item>
      <title>프로젝트 티켓 예매 시스템 개선하기</title>
      <link>https://hechan2.tistory.com/32</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;축제 SaaS 서비스 프로젝트는 고객마다 하나의 축제를 등록하고, 각 축제의 메인페이지를 위젯 단위로 구성해 축제 분위기에 맞는 사이트를 만들어주는 서비스입니다. 그러던 중 고객사로부터 &lt;b&gt;티켓 예매 기능&lt;/b&gt;&amp;nbsp;요청이 들어왔고, 빠른 일정 안에 비관적 락 기반의 예매 시스템을 구현하여 실제 목포 W쇼 티켓팅에 도입했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 초과 발급은 발생하지 않았지만, 운영 과정에서 응답 지연과 커넥션 풀 고갈 문제가 드러났습니다. 이 글에서는 비관적 락을 선택한 배경부터, 기존 코드의 문제 분석, 그리고 두 번의 걸친 개선 과정을 정리해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 비관적 락 도입 배경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;0인데도&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;JPA 환경에서 DB 레벨의 동시성 제어 방법은 크게 두 가지다. `@Version`을 사용하는 &lt;b&gt;낙관적 락&lt;/b&gt;과, `@Lock(PESSIMISTIC_WRITE)`를 사용하는 &lt;b&gt;비관적 락(Pessimistic Lock)&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;1224&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EgQXw/dJMcaiI1XK3/GQ4CV6moOrjNP6JzluCO2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EgQXw/dJMcaiI1XK3/GQ4CV6moOrjNP6JzluCO2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EgQXw/dJMcaiI1XK3/GQ4CV6moOrjNP6JzluCO2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEgQXw%2FdJMcaiI1XK3%2FGQ4CV6moOrjNP6JzluCO2K%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;555&quot; height=&quot;144&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 예매는 오픈 시점에 &lt;b&gt;충돌이 거의 100% 발생&lt;/b&gt;하는 시나리오다. 낙관적 락을 적용하면 대부분의 요청이 버전 충돌로 실패하고, 클라이언트가 재시도해야 한다. 재시도가 몰리면 충돌은 더 심해지고, 사용자 경험은 최악이 된다고 생각했다.&lt;br /&gt;&lt;br /&gt;반면 비관적 락은 DB 레벨에서 순서를 보장하므로, &lt;b&gt;한 번 대기하면 반드시 처리된다&lt;/b&gt;는 확실성이 있다. 빠른 일정 안에서 정합성을 확실히 보장하는 선택이 필요했기 때문에 비관적 락을 채택했다.&lt;br /&gt;&lt;br /&gt;실제로 목포 W쇼 티켓팅에서 초과 판매는 발생하지 않았고, 비관적 락의 정합성은 검증되었다. 하지만 운영 중 다음 두 가지 문제가 드러났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;응답 지연&lt;/b&gt;: 락 경합이 심해지면서 사용자 응답 시간이 수 초~수십 초로 늘어났다. 락을 잡은 채로 불필요한 쿼리를 실행하고 있었기 때문이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커넥션 풀 고갈&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;: 트랜잭션 내부에서 SMS 외부 API를 호출하고 있었고, SMS 응답을 기다리는 동안 DB 커넥션이 묶여 전체 서비스가 느려졌다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌이켜보면 이 문제들은 처음 코드를 작성할 때 트랜잭션 경계와 락 범위에 대한 이해가 부족해서 발생한 것이었다. 이렇게라도 실제 운영을 통해 문제점을 직접 파악하고 개선할 수 있었다는 점에서 좋은 경험이었다고 생각한다. &lt;br /&gt;&lt;br /&gt;이 문제들의 원인을 실제 코드 레벨에서 분석해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 기존 코드의 문제 분석&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 간단하게 이 예매 시스템에 대해 설명해보면, SMS 인증 후 게스트 JWT 토큰을 발급하는 방식으로 동작한다. 토큰에는 유효기간이 존재하는데, 미리 발급받은 토큰으로 오픈 전에 API 요청을 보내는 것을 막기 위해 예매 시점에 인증 상태를 다시 검증해야 했다. 그래서 당시에는 &lt;b&gt;검증을 최대한 철저하게&lt;/b&gt; 하는 방향으로 코드를 작성했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제&amp;nbsp;1:&amp;nbsp;불필요하게&amp;nbsp;긴&amp;nbsp;락&amp;nbsp;홀딩&amp;nbsp;시간&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존&amp;nbsp;`LockBasedTicketService.reserve()`&amp;nbsp;메서드를&amp;nbsp;보자.&lt;/p&gt;
&lt;pre id=&quot;code_1771416961186&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BEFORE: LockBasedTicketService.java
@Override
@Transactional
public void reserve(TicketRequest request) {
    validateVerification(request);                              // ① DB 조회 (인증 확인)
    Event event = loadEventWithLockOrThrow(request.getEventId()); // ② 락 획득 (FOR UPDATE)
    event.validateIsOpenAt();                                    // ③ 오픈 시간 검증
    isExistTicketBy(request.getEventId(), request.getPhoneNumber()); // ④ 중복 예매 조회
    event.decreaseRemainingTickets(request.getTicketCount());    // ⑤ 재고 차감
    ticketRepository.save(Ticket.of(request, event));            // ⑥ 티켓 저장
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도 자체는 틀리지 않았지만, 문제는 &lt;b&gt;검증과 락의 순서&lt;/b&gt;에 있었다. &lt;b&gt;②에서 락을 획득&lt;/b&gt;한 뒤, &lt;b&gt;④에서 중복 예매 여부를 조회&lt;/b&gt;하고 있다. `isExistTicketBy()`는 단순 읽기 쿼리임에도 락을 잡은 채로 실행되므로, 다른 모든 스레드가 이 쿼리가 끝날 때까지 대기해야 한다. &lt;br /&gt;&lt;br /&gt;트래픽이 폭주하는 시점에 이 차이는 크게 벌어진다. 100명이 동시에 예매를 시도하면, 99명은 한 명의 검증 쿼리가 끝나기를 기다려야 하고, 이 대기 시간이 누적되어 &lt;b&gt;전체 처리량(throughput)이 급격히 떨어진다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제&amp;nbsp;2:&amp;nbsp;트랜잭션&amp;nbsp;내부에서&amp;nbsp;SMS&amp;nbsp;발송&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1771425031381&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void sendVerificationCode(VerificationReqDto verificationReqDto) {
    validateEventIsActive(verificationReqDto.getEventId());

    Verification verification = verificationRepository.findByPhoneNumber(...)
            .map(existing -&amp;gt; {
                existing.updateVerificationCode();
                return existing;
            })
            .orElseGet(() -&amp;gt; verificationRepository.save(Verification.from(verificationReqDto)));

    smsClient.send(SmsSendRequest.from(verification)); // 외부 API 호출이 트랜잭션 안에!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;코드에는&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;심각한&amp;nbsp;문제가&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;첫째, 커넥션 풀 고갈 위험&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`@Transactional` 메서드가 시작되면 DB 커넥션을 하나 점유한다. SMS 발송은 외부 네트워크 I/O로, 통상 1~5초가 걸린다. 이 시간 동안 DB 커넥션이 묶여 있으므로, 동시 요청이 쏟아지면 커넥션 풀이 빠르게 고갈된다.&lt;/p&gt;
&lt;pre id=&quot;code_1771425072043&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[요청 1] ── DB 저장(10ms) ── SMS 발송(3000ms) ── 커밋 ──&amp;rarr;  총 3010ms 커넥션 점유
[요청 2] ── DB 저장(10ms) ── SMS 발송(2500ms) ── 커밋 ──&amp;rarr;  총 2510ms 커넥션 점유
[요청 3] ── 커넥션 대기... ─────────────────────────&amp;rarr;  HikariCP timeout!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;둘째, 불필요한 롤백&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SMS 발송이 실패하면 `smsClient.send()`에서 예외가 발생하고, `@Transactional`에 의해 인증코드 저장까지 롤백된다. 하지만 인증코드는 이미 생성되었으므로, SMS만 재시도하면 되는 상황이다. DB까지 롤백하면 사용자는 처음부터 다시 요청해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제&amp;nbsp;3:&amp;nbsp;DB&amp;nbsp;비관적&amp;nbsp;락의&amp;nbsp;커넥션&amp;nbsp;풀&amp;nbsp;고갈&amp;nbsp;위험&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든&amp;nbsp;동시성&amp;nbsp;제어가&amp;nbsp;`@Lock(PESSIMISTIC_WRITE)`,&amp;nbsp;즉&amp;nbsp;`SELECT&amp;nbsp;...&amp;nbsp;FOR&amp;nbsp;UPDATE`&amp;nbsp;하나에&amp;nbsp;의존하고&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771428383716&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// EventRepository.java
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(&quot;SELECT e FROM Event e WHERE e.id = :id&quot;)
Optional&amp;lt;Event&amp;gt; findByIdWithLock(@Param(&quot;id&quot;) Long id);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;DB 비관적 락은 정합성을 확실히 보장하지만, 구조적으로 &lt;b&gt;락 대기가 곧 커넥션 점유&lt;/b&gt;라는 문제가 다. `SELECT FOR UPDATE`는 DB 커넥션을 잡은 상태에서 락 획득을 대기한다. 즉, 100명이 동시에 예매를 시도하면 100개의 DB 커넥션이 동시에 점유되고, 그 중 99개는 락이 풀릴 때까지 아무 일도 하지 않으면서 커넥션만 잡고 있는 셈이다.&lt;/p&gt;
&lt;pre id=&quot;code_1771428412588&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[스레드 1] ── 커넥션 획득 ── FOR UPDATE (락 획득) ── 처리 ── 커밋 ── 커넥션 반환
[스레드 2] ── 커넥션 획득 ── FOR UPDATE (대기중...) ─────────────── 커넥션 점유 중
[스레드 3] ── 커넥션 획득 ── FOR UPDATE (대기중...) ─────────────── 커넥션 점유 중
   ...
[스레드 N] ── 커넥션 획득 실패 &amp;rarr; HikariCP timeout!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;HikariCP의 기본 커넥션 풀 크기는 10개다. 동시 요청이 10개를 넘는 순간 나머지 요청은&lt;b&gt; 커넥션 자체를 획득하지 못해&lt;/b&gt; 락 경합과는 별개로 타임아웃이 발생한다. 이전까지는 쿼리 타임아웃으로 커넥션을 반납하도록 코드를 작성했지만, 트래픽이 더 증가하면 근본적인 해결이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 첫 번째 개선 : 트랜잭션 및 동시성 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드를 &lt;b&gt;구조적으로 변경하지 않으면서&lt;/b&gt; 동시성과 트랜잭션 문제를 해결해보려 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개선&amp;nbsp;1:&amp;nbsp;락&amp;nbsp;홀딩&amp;nbsp;시간&amp;nbsp;최소화&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1771425413626&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AFTER: LockBasedTicketService.java
@Override
@Transactional
public void reserve(TicketRequest request) {
    // ── 검증 구간 (락 없이 수행) ──
    validateVerification(request);                              // ① 인증 확인
    isExistTicketBy(request.getEventId(), request.getPhoneNumber()); // ② 중복 예매 확인

    // ── 임계 구간 (락 획득 후 변경) ──
    Event event = loadEventWithLockOrThrow(request.getEventId()); // ③ 락 획득
    event.validateIsOpenAt();                                    // ④ 오픈 시간 (빠른 체크)
    event.decreaseRemainingTickets(request.getTicketCount());    // ⑤ 재고 차감
    ticketRepository.save(Ticket.of(request, event));            // ⑥ 저장
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;변경한 점&lt;/b&gt; :&amp;nbsp; `isExistTicketBy()`를 락 획득 전(②)으로 이동했다.&lt;/p&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 예매 확인은 &lt;b&gt;읽기 작업&lt;/b&gt;이다. 락 밖에서 사전 필터링하면, 이미 예매한 사용자는 락을 잡지도 않고 빠르게 탈락시킬 수 있다.&lt;/li&gt;
&lt;li&gt;단, 중복 체크를 락 밖으로 빼면 동시 요청 시 두 건 모두 통과할 수 있으므로, Ticket 테이블에 `(event_id, phone_number)` &lt;b&gt;유니크 제약조건&lt;/b&gt;을 추가하여 DB 레벨에서 중복을 최종 방어했다.&lt;/li&gt;
&lt;li&gt;검증을 락 밖으로 빼면 &lt;b&gt;락 홀딩 시간이 검증 쿼리 시간만큼 줄어든다. &lt;/b&gt;검증 쿼리가 50ms라면 100건 기준 총 5초의 대기 시간이 절약된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771425568576&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BEFORE: ──[락 획득]──[검증 쿼리 50ms]──[재고 차감 5ms]──[저장 10ms]──[락 해제]──
          └──────────── 락 홀딩: 65ms ────────────────┘

AFTER:  ──[검증 쿼리 50ms]──[락 획득]──[재고 차감 5ms]──[저장 10ms]──[락 해제]──
                              └─────── 락 홀딩: 15ms ──────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개선&amp;nbsp;2:&amp;nbsp;트랜잭션&amp;nbsp;경계&amp;nbsp;재설계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 문제 분석에서 살펴봤듯이, 기존 `sendVerificationCode()`는 `@Transactional` 안에서 SMS 외부 API를 호출하고 있었다. SMS 응답을 기다리는 수 초 동안 DB 커넥션이 묶여 커넥션 풀이 고갈되고, SMS 실패 시 인증코드까지 롤백되는 문제가 있었다. 해결 방향은 단순하다. &lt;b&gt;DB 작업과 외부 I/O를 분리&lt;/b&gt;하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 `private` 메서드에 `@Transactional`을 붙여서 DB 저장과 SMS 발송을 분리하는 방식을 시도했다. 하지만 여기에는 근본적인 문제가 있다. &lt;b&gt;Spring&amp;nbsp;AOP는&amp;nbsp;프록시&amp;nbsp;기반으로&amp;nbsp;동작&lt;/b&gt;하기 때문에, `private` 메서드에 `@Transactional`을 붙여도 실제로는 트랜잭션이 적용되지 않는다. 같은 클래스 내부에서 `this.saveVerificationCode()`를 호출하면 프록시를 거치지 않고 직접 호출되기 때문이다. &lt;br /&gt;&lt;br /&gt;이 문제를 근본적으로 해결하기 위해 &lt;b&gt;Spring의 `@TransactionalEventListener`&lt;/b&gt;를 도입했다. 트랜잭션이 커밋된 후에 SMS가 발송되는 것을 프레임워크 레벨에서 보장한다.&lt;/p&gt;
&lt;pre id=&quot;code_1771425777092&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이벤트 클래스
@Getter
@RequiredArgsConstructor
public class VerificationCodeSavedEvent {
    private final String phoneNumber;
    private final String code;
}

// AFTER: VerificationService.java
@Transactional
public void sendVerificationCode(VerificationReqDto verificationReqDto) {
    validateEventIsActive(verificationReqDto.getEventId());

    Verification verification = verificationRepository.findByPhoneNumber(verificationReqDto.getPhoneNumber())
            .map(existing -&amp;gt; {
                existing.updateVerificationCode();
                return existing;
            })
            .orElseGet(() -&amp;gt; verificationRepository.save(Verification.from(verificationReqDto)));

    eventPublisher.publishEvent(
            new VerificationCodeSavedEvent(verification.getPhoneNumber(), verification.getCode()));
}

// 이벤트 리스너 &amp;mdash; 커밋 후 SMS 발송
@Component
@RequiredArgsConstructor
public class VerificationEventListener {
    private final SmsClient smsClient;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleVerificationCodeSaved(VerificationCodeSavedEvent event) {
        try {
            smsClient.send(SmsSendRequest.ofVerification(event.getPhoneNumber(), event.getCode()));
        } catch (Exception e) {
            log.error(&quot;SMS send failed for phone: {}&quot;, event.getPhoneNumber(), e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 결정한 근거를 정리하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1407&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cylJtA/dJMcajuppTO/YFb3lPRSblPRFP3XXaDQS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cylJtA/dJMcajuppTO/YFb3lPRSblPRFP3XXaDQS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cylJtA/dJMcajuppTO/YFb3lPRSblPRFP3XXaDQS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcylJtA%2FdJMcajuppTO%2FYFb3lPRSblPRFP3XXaDQS1%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;555&quot; height=&quot;157&quot; data-origin-width=&quot;1407&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 원칙은 &lt;b&gt;외부 I/O는 트랜잭션 커밋 후에, DB 작업만 트랜잭션 안에서&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`@TransactionalEventListener(phase = AFTER_COMMIT)`를 사용하면 SMS 발송이 트랜잭션 커밋 이후에 실행되는 것을 Spring이 보장하므로, 호출 순서나 프록시 동작 여부에 의존하지 않는 안정적인 구조가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 두 번째 개선 : 비관적 락 -&amp;gt; 메모리 락으로 전환 및 락 추상화&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 개선이 &lt;b&gt;기존 구조 안에서의 최적화&lt;/b&gt;였다면, 두 번째 개선은 &lt;b&gt;락 메커니즘 자체를 전환&lt;/b&gt;하는 작업이다. &lt;br /&gt;&lt;br /&gt;첫 번째 개선으로 락 홀딩 시간과 트랜잭션 경계 문제는 해결했지만, 앞서 문제 분석(섹션 3)에서 짚었던 &lt;b&gt;근본적인 한계&lt;/b&gt;는 그대로 남아 있었다. DB 비관적 락은 `SELECT FOR UPDATE`로 락을 대기하는 동안 DB 커넥션을 점유하기 때문에, 동시 요청이 커넥션 풀 크기를 넘는 순간 커넥션 고갈이 발생한다. 락 홀딩 시간을 아무리 줄여도, &lt;b&gt;락 대기 = 커넥션 점유&lt;/b&gt;라는 구조 자체가 바뀌지 않는 한 트래픽 증가에 취약할 수밖에 없다. &lt;br /&gt;&lt;br /&gt;처음에는 Redis 기반 분산 락(Redisson)을 도입하려고 했다. 분산 락은 DB 커넥션 없이 락을 대기할 수 있고, 멀티 인스턴스 환경에서도 동작하니 가장 확실한 해결책이었다. 하지만 한 발 물러서 생각해보니, &lt;b&gt;현재 Festimap의 티켓팅 서버는 단일 인스턴스로 운영&lt;/b&gt;되고 있었다. Redis를 도입하면 인프라 비용이 증가하고 운영 복잡도도 올라간다. 단일 인스턴스에서 분산 락은 명백한 오버 엔지니어링이었다. &lt;br /&gt;&lt;br /&gt;그래서 방향을 전환했다. 지금 당장은 JVM의 `ReentrantLock`을 사용하는 &lt;b&gt;메모리 락&lt;/b&gt;으로 커넥션 고갈 문제를 해결하고, 나중에 트래픽이 커져 서버를 스케일 아웃할 때를 대비해 &lt;b&gt;락 메커니즘을 인터페이스로 추상화&lt;/b&gt;하기로 했다. 이렇게 하면 지금은 추가 인프라 없이 문제를 해결하면서도, 분산 락이 필요한 시점에 서비스 코드 변경 없이 구현체만 교체할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개선 : 락 추상화 with 전략 패턴&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&amp;nbsp;락의&amp;nbsp;획득과&amp;nbsp;해제를&amp;nbsp;`LockManager`&amp;nbsp;인터페이스로&amp;nbsp;정의했다.&amp;nbsp;서비스&amp;nbsp;계층은&amp;nbsp;이&amp;nbsp;인터페이스에만&amp;nbsp;의존하고,&amp;nbsp;실제&amp;nbsp;락&amp;nbsp;구현이&amp;nbsp;메모리인지&amp;nbsp;Redis인지는&amp;nbsp;알&amp;nbsp;필요가&amp;nbsp;없다.&lt;/p&gt;
&lt;pre id=&quot;code_1771429352503&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LockManager.java &amp;mdash; 락 추상화 인터페이스
public interface LockManager {
    boolean tryLock(String key, long waitTime, long leaseTime, TimeUnit timeUnit)
            throws InterruptedException;
    void unlock(String key);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재&amp;nbsp;사용하는&amp;nbsp;구현체는&amp;nbsp;JVM의&amp;nbsp;`ReentrantLock`을&amp;nbsp;활용한&amp;nbsp;`LocalLockManager`다.&amp;nbsp;`ConcurrentHashMap`으로&amp;nbsp;key별&amp;nbsp;락을&amp;nbsp;관리하며,&amp;nbsp;DB&amp;nbsp;커넥션&amp;nbsp;없이&amp;nbsp;메모리에서&amp;nbsp;락&amp;nbsp;대기가&amp;nbsp;이루어진다.&lt;/p&gt;
&lt;pre id=&quot;code_1771429382944&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class LocalLockManager implements LockManager {
    private final ConcurrentHashMap&amp;lt;String, ReentrantLock&amp;gt; locks = new ConcurrentHashMap&amp;lt;&amp;gt;();

    @Override
    public boolean tryLock(String key, long waitTime, long leaseTime, TimeUnit timeUnit)
            throws InterruptedException {
        ReentrantLock lock = locks.computeIfAbsent(key, k -&amp;gt; new ReentrantLock());
        boolean acquired = lock.tryLock(waitTime, timeUnit);
        if (acquired) log.debug(&quot;Lock acquired for key: {}&quot;, key);
        return acquired;
    }

    @Override
    public void unlock(String key) {
        ReentrantLock lock = locks.get(key);
        if (lock != null &amp;amp;&amp;amp; lock.isHeldByCurrentThread()) {
            lock.unlock();
            // 대기 중인 스레드가 없으면 메모리에서 제거
            if (!lock.hasQueuedThreads()) {
                locks.remove(key);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&amp;nbsp;나중에&amp;nbsp;분산&amp;nbsp;환경으로&amp;nbsp;전환할&amp;nbsp;때를&amp;nbsp;대비해&amp;nbsp;`RedisLockManager`&amp;nbsp;스켈레톤도&amp;nbsp;준비해두었다.&lt;/p&gt;
&lt;pre id=&quot;code_1771429416379&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// @Component
// @Profile(&quot;prod&quot;)
public class RedisLockManager implements LockManager {
    // TODO: Redisson 라이브러리 추가 후 구현
    //   RLock lock = redissonClient.getLock(key);
    //   return lock.tryLock(waitTime, leaseTime, timeUnit);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, `LockManager` 인터페이스를 두고 `LocalLockManager`(메모리 락)와 `RedisLockManager`(분산 락) 두 구현체를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전략 패턴으로 구성했다. 현재는 `LocalLockManager`가 활성화되어 있고, 분산 환경이 필요한 시점에 `@Profile`만 전환하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서비스 코드 변경 없이&lt;/b&gt; 분산 락으로 교체할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 최종 정리 : 무엇이 바뀌었는가?&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번의 개선에 걸쳐 세 가지 문제를 해결했다. 각 문제가 어떻게 변했는지 정리해보자.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;문제 1: 불필요하게 긴 락 홀딩 시간 &amp;rarr; 검증을 락 밖으로 이동&lt;/b&gt;&lt;br /&gt;읽기&amp;nbsp;전용&amp;nbsp;검증(`isExistTicketBy`)을&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;요청&amp;nbsp;시&amp;nbsp;대기열에서&amp;nbsp;기다리는&amp;nbsp;시간이&amp;nbsp;검증&amp;nbsp;쿼리&amp;nbsp;수만큼&amp;nbsp;줄어든다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제 2: 트랜잭션 내부 SMS 발송 &amp;rarr; @TransactionalEventListener로 커밋 후 분리&lt;/b&gt;&lt;br /&gt;DB&amp;nbsp;저장과&amp;nbsp;SMS&amp;nbsp;발송을&amp;nbsp;이벤트&amp;nbsp;기반으로&amp;nbsp;분리했다.&amp;nbsp;`@TransactionalEventListener(phase&amp;nbsp;=&amp;nbsp;AFTER_COMMIT)`로&amp;nbsp;&lt;br /&gt;트랜잭션&amp;nbsp;커밋&amp;nbsp;후에&amp;nbsp;SMS가&amp;nbsp;발송되는&amp;nbsp;것을&amp;nbsp;Spring이&amp;nbsp;보장한다.&amp;nbsp;DB&amp;nbsp;커넥션&amp;nbsp;점유&amp;nbsp;시간이&amp;nbsp;3~5초에서&amp;nbsp;10~50ms로&amp;nbsp;&lt;br /&gt;줄었고,&amp;nbsp;SMS&amp;nbsp;실패&amp;nbsp;시에도&amp;nbsp;인증코드가&amp;nbsp;롤백되지&amp;nbsp;않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제 3: DB 비관적 락의 커넥션 고갈 &amp;rarr; 메모리 락 전환 + 락 추상화&lt;br /&gt;&lt;/b&gt;DB&amp;nbsp;`SELECT&amp;nbsp;FOR&amp;nbsp;UPDATE`에서&amp;nbsp;JVM&amp;nbsp;`ReentrantLock`&amp;nbsp;기반&amp;nbsp;메모리&amp;nbsp;락으로&amp;nbsp;전환했다.&amp;nbsp;락&amp;nbsp;대기가&amp;nbsp;DB&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;`LockManager`&amp;nbsp;&lt;br /&gt;인터페이스로&amp;nbsp;추상화하여&amp;nbsp;분산&amp;nbsp;환경&amp;nbsp;전환&amp;nbsp;시&amp;nbsp;구현체만&amp;nbsp;교체하면&amp;nbsp;된다.&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1513&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sIta0/dJMcabJX7X6/55EDkjiyEjUKcLKiH6OO2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sIta0/dJMcabJX7X6/55EDkjiyEjUKcLKiH6OO2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sIta0/dJMcabJX7X6/55EDkjiyEjUKcLKiH6OO2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsIta0%2FdJMcabJX7X6%2F55EDkjiyEjUKcLKiH6OO2k%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;555&quot; height=&quot;213&quot; data-origin-width=&quot;1513&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 마무리 : 이 과정에서 배운 것&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 티켓 예매 시스템을 구현할 때는 동시성 제어를&amp;nbsp;&lt;b&gt;낙관적 락 vs 비관적 락&lt;/b&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;생각했다. &lt;br /&gt;&lt;br /&gt;하지만 실제 운영을 겪으면서 시야가 넓어졌다. 락 자체보다 &lt;b&gt;락을 잡고 있는 동안 무엇이 일어나는지&lt;/b&gt;가 더 중요했다. 락 안에서 불필요한 쿼리를 실행하면 대기 시간이 누적되고, 트랜잭션 안에서 외부 API를 호출하면 커넥션이 묶인다. 더 나아가 DB 비관적 락은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;락을 기다리는 것 자체가 커넥션을 점유&lt;/b&gt;하기 때문에, 동시 요청이 커넥션 풀 크기를 넘는 순간 락 경합과는 별개로 시스템이 멈출 수 있다는 것도 알게 되었다. &lt;br /&gt;&lt;br /&gt;결국 이번 개선 과정은 &lt;b&gt;트랜잭션 경계, 락 범위, 커넥션 풀&lt;/b&gt;이라는 세 가지가 서로 밀접하게 연결되어 있다는 것을 체감한 경험이었다. 락의 종류를 고르는 것은 시작일 뿐이고, 그 락이 트랜잭션과 커넥션에 미치는 영향까지 함께 설계해야 한다. &lt;br /&gt;&lt;br /&gt;여기서 한 발 더 나아가 생각해보면, 현재 구조는 메모리 락으로 커넥션 고갈을 해결했지만 여전히 &lt;b&gt;동기 처리 방식&lt;/b&gt;이다. 100명이 동시에 요청하면 99명은 메모리에서 락이 풀릴 때까지 대기해야 한다. 만약 트래픽 규모가 지금보다 훨씬 커진다면, 메시지 큐를 도입해 예매 요청을 비동기로 처리하는 방식도 고려해볼 수 있을 것이다. 요청을 큐에 넣고 순차 처리하면 락 자체가 필요 없어지고, 사용자에게는 즉시 응답을 돌려줄 수 있다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>Spring Boot</category>
      <category>동시성</category>
      <category>비관적 락</category>
      <category>커넥션 풀</category>
      <category>트랜잭션</category>
      <author>chan_2</author>
      <guid isPermaLink="true">https://hechan2.tistory.com/32</guid>
      <comments>https://hechan2.tistory.com/32#entry32comment</comments>
      <pubDate>Thu, 19 Feb 2026 01:13:23 +0900</pubDate>
    </item>
    <item>
      <title>코딩 테스트에서 잘못 사용했던 Object.clone()</title>
      <link>https://hechan2.tistory.com/31</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Java로 코딩테스트 문제를 풀던 중, 2차원 배열을 clone() 메서드로 복제한 후 수정하는 로직에서 원본 배열까지 함께 변경되는 문제를 경험했다. 당시에는 &quot;clone이 얕은 복사를 수행하기 때문&quot;이라는 표면적인 이해만으로 넘어갔고, 2차원 배열은 무조건 2중 반복문으로 복사해야겠다는 생각만 가진 채 문제를 마무리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 『이펙티브 자바』와 『이것이 자바다』를 읽으며 배열의 메모리 구조와 clone()의 동작 원리를 깊이 있게 학습할 기회가 있었다. 힙 메모리에서 1차원 배열과 2차원 배열이 각각 어떻게 관리되는지, 그에 따라 clone()이 어떻게 동작하는지를 이해하면서 가변 객체, 불변 객체, 깊은 복사, 얕은 복사의 개념을 명확히 정리할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 학습한 내용을 바탕으로 clone() 메서드의 동작 방식을 정리하고, &lt;b&gt;코딩테스트에서 clone을 안전하게 사용하는 방법&lt;/b&gt;을 명확히 정리하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.&amp;nbsp; 오버라이딩이&amp;nbsp; 필요한 Object 메서드들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Java 클래스에서 기본적으로 상속받는 클래스가 없다면 Object를 상속받는다. 이때, 커스텀 클래스의 경우 equals(), hashCode(), toString() 등과 같은 메서드가&amp;nbsp; 정상적으로 동작하도록 보장하기 위해서는 오버라이딩이 필수다. toString()을 정의하지 않고 썼을 때 아래 사진과 같이 출력되는 경우를 본 적이 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lp3Zy/dJMcafETURq/6uCK8tAtdFFKum5IJAz60k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lp3Zy/dJMcafETURq/6uCK8tAtdFFKum5IJAz60k/img.png&quot; data-alt=&quot;기본 toString()&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lp3Zy/dJMcafETURq/6uCK8tAtdFFKum5IJAz60k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flp3Zy%2FdJMcafETURq%2F6uCK8tAtdFFKum5IJAz60k%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;850&quot; height=&quot;230&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기본 toString()&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;따라서, 커스텀 클래스에서 만약 이런 Object가 제공하는 메서드를 사용할 상황이 있다면 정상적으로 동작하도록 규약에 맞게 재정의 해줘야 한다. clone 메서드도 이런 오버라이드를 해줘야 하는 메서드 중 하나이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Object.clone() 메서드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 메서드들과는 다르게 특이하게도 clone()의 경우는 오버라이딩해서 사용하려면 커스텀 클래스에 Clonable이라는 인터페이스를 implements 해야 한다. Cloneable 인터페이스를 살펴보면 아래와 같이 아무 메서드도 갖고 있지 않다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CWFrU/dJMcadAjJr8/MiGRWKoNZMXPc43IIbwhMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CWFrU/dJMcadAjJr8/MiGRWKoNZMXPc43IIbwhMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CWFrU/dJMcadAjJr8/MiGRWKoNZMXPc43IIbwhMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCWFrU%2FdJMcadAjJr8%2FMiGRWKoNZMXPc43IIbwhMk%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;850&quot; height=&quot;500&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Object의 clone()에 적혀있는 문서 내용에는 아래와 같이 적혀있다. 해석해 보면 Cloneable 인터페이스를 지원하지 않는다면 CloneNotSupportedException을 던진다고 나와있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kQyII/dJMcadtx9go/rvQzVik8bp2t8SXYH7eso0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kQyII/dJMcadtx9go/rvQzVik8bp2t8SXYH7eso0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kQyII/dJMcadtx9go/rvQzVik8bp2t8SXYH7eso0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkQyII%2FdJMcadtx9go%2FrvQzVik8bp2t8SXYH7eso0%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;850&quot; height=&quot;329&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 동작도 궁금해 아래와 같이 코드를 작성해 실험해 보았는데 문서대로 동작하는 것을 확인할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762227157368&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Food {
    private String name;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

// 실행 코드
Food food = new Food();
food.clone();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E0Dwq/dJMcacak2yi/Zb0XXObXpCf0I0Yf5JvazK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E0Dwq/dJMcacak2yi/Zb0XXObXpCf0I0Yf5JvazK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E0Dwq/dJMcacak2yi/Zb0XXObXpCf0I0Yf5JvazK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE0Dwq%2FdJMcacak2yi%2FZb0XXObXpCf0I0Yf5JvazK%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;850&quot; height=&quot;156&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이펙티브 자바에서는 이런 clone()을 강제할 수 없고, 허술하게 기술된 프로토콜이라고 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&lt;b&gt;a. 강제할 수 없는 규약&lt;/b&gt;`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloneable는 메서드가 하나도 없는 마커 인터페이스다. 즉 clone() 메서드를 구현하도록 강제할 수 없다. 인터페이스를 사용하는 이유는 추상화, 다형성도 있지만 컴파일러 입장에서는 구현해야 할 메서드를 강제한다는 관점을 갖고 있다. 이런 관점에서 잘못되었다고 비판한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&lt;b&gt;b. 허술하게 기술된 프로토콜&lt;/b&gt;`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;clone을 오버라이딩할 때 super.clone()과 new로 생성하는 것이 동작상 차이가 없기 때문에 컴파일 타임에 이를 감지할 수 없으며 런타임에 실제로 잘못 구현된 clone이 있으면 문제가 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1762228370694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Parent implements Cloneable {
    String name;
    
    @Override
    public Parent clone() {
        //super.clone()을 호출하지 않고 생성자로 반환
        Parent p = new Parent();
        p.name = this.name;
        return p;
    }
}

class Child extends Parent {
    int age;
    
    @Override
    public Child clone() {
        // super.clone()을 호출
        Child c = (Child) super.clone();
        c.age = this.age;
        return c;
    }
}

// 실행 코드
Child child = new Child();
child.name = &quot;Alice&quot;;
child.age = 10;
Child cloned = child.clone();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 실제로 실행시켜 보면 아래와 같이 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sTejg/dJMcaelGTmG/D3bShOtRSE1ro31K6TkSK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sTejg/dJMcaelGTmG/D3bShOtRSE1ro31K6TkSK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sTejg/dJMcaelGTmG/D3bShOtRSE1ro31K6TkSK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsTejg%2FdJMcaelGTmG%2FD3bShOtRSE1ro31K6TkSK0%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;850&quot; height=&quot;148&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 문제가 발생하는 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 인스턴스로 메서드 호출 시 저장되는 this에 있었다. child.clone()을 호출하면 스택프레임에 this는 child를 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 child의 clone에서 super.clone()을 호출할 때에도, parent.clone()의 스택 프레임에 this는 child이다. Object.clone()은 네이티브 메서드로, 내부적으로 this.getClass()를 통해 실제 객체의 타입을 확인하고 &lt;b&gt;B 타입의 새로운 인스턴스를 생성&lt;/b&gt;한다. (반환 타입은 Object)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 간단하게나마 확인할 수 있는 코드를 아래처럼 작성해 보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1762229739296&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class A implements Cloneable{
    int a;

    @Override
    public A clone() throws CloneNotSupportedException {
        System.out.println(&quot;A method call : &quot;+this.getClass());
        A a =(A)super.clone();
        System.out.println(&quot;Object.clone() return instance : &quot;+a.getClass());
        return a;
    }
}

class B extends A implements Cloneable{
    int b;

    @Override
    public B clone() throws CloneNotSupportedException {
        System.out.println(&quot;B method call : &quot;+ this.getClass());
        return (B)super.clone();
    }
}


//실행 코드
B b = new B();
b.clone();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qLRJC/dJMcake8IGA/Ir8rrStOwUXK2hyxdlbltk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qLRJC/dJMcake8IGA/Ir8rrStOwUXK2hyxdlbltk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qLRJC/dJMcake8IGA/Ir8rrStOwUXK2hyxdlbltk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqLRJC%2FdJMcake8IGA%2FIr8rrStOwUXK2hyxdlbltk%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;850&quot; height=&quot;207&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 만약 A의 clone에서 super.clone()을 호출하지 않고&amp;nbsp; A 인스턴스를 new 생성자를 통해 생성하고 이를 return 하도록 구현했다면? return 받은 A 객체를 (B)로 형변환이 불가능하기 때문에 ClassCastException이 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 실제로 이펙티브 자바에서 말한 내용에 대해 코드를 작성해 보며 느낀 점은 clone은 정말 잘못 사용하면 끝도 없이 문제가 발생할 수 있겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 불변 객체와 가변 객체, 그리고 clone()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clone() 메서드는 가변 객체를 다룰 때 특히 주의가 필요하다. 이를 이해하기 위해 먼저 불변 객체와 가변 객체의 차이를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;불변 객체&lt;/b&gt;란?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 불변 객체는 &lt;b&gt;생성 후 상태가 변경되지 않는 객체&lt;/b&gt;를 말한다. 대표적인 예가 String이다.&lt;/p&gt;
&lt;pre id=&quot;code_1762234790269&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String a = &quot;Hello&quot;;
a = &quot;Bye&quot;;  // 기존 객체를 수정하는 것이 아니라 새로운 객체를 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 a = &quot;Bye&quot;는 기존 &quot;Hello&quot; 객체를 수정하는 것이 아니라, 내부적으로 new String(&quot;Bye&quot;) 객체를 생성하고 그 참조를 a에 할당한다. 불변 객체는 모든 필드가 final로 선언되어 있으며, String의 경우 문자열 상수 풀을 통해 동일한 문자열을 재사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가변 객체&lt;/b&gt;는 반대로 생성 후에도 상태가 변경 가능한 객체를 말한다. 대표적으로 아래와 같이 작성된 클래스에서 List 객체가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1762231805312&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person implements Cloneable {
    private String name;
    private List&amp;lt;String&amp;gt; hobbies = new ArrayList&amp;lt;&amp;gt;();
	
    ...
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Person 객체를 생성하고 해당 객체를 이용해 clone()을 호출하면 String의 경우 불변객체라 새로운 person 객체의 name을 생성할 때 같은 ref를 사용해도 문제가 되지 않는다. 실제로 문자열 상수풀을 이용하여 String의 경우 한번 사용된 문자열의 경우 새로 생성하지 않고 재사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, hobbies의 경우 같은 ref를 복사해 생성할 경우, element를 하나 추가할 때, 그것이 원본 Person 객체에도 영향이 가기 때문에 문제가 된다. 이는 clone을 아래와 같이 사용할 때 &lt;b&gt;얕은 복사로 동작&lt;/b&gt;하기 때문에 발생하는 문제이다(hobbies 객체의 ref만 복사하고 내부의 element는 신경 쓰지 않음).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 코드를 작성하고 확인해 보면 명확하다.&lt;/p&gt;
&lt;pre id=&quot;code_1762233135161&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person implements Cloneable {
    private String name;
    private List&amp;lt;String&amp;gt; hobbies = new ArrayList&amp;lt;&amp;gt;();

	...

    @Override
    public Person clone() {
        try {
            return (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
    
    ...
    
}

// 실행 코드
public static void mutableObjectClone(){
    Person person = new Person(&quot;Lee&quot;);
    person.addHobby(&quot;Game&quot;);
    person.addHobby(&quot;Book&quot;);
    Person clonePerson = person.clone();

    // 1. 동일성 검사시 같은 레퍼런스를 참조하고 있을 것이기 때문에 true여야 한다.
    System.out.println(&quot;name reference same : &quot;+(person.getName() == clonePerson.getName()));
    person.setName(&quot;Kim&quot;);

    //2. String은 불변객체 따라서, reference값이 변경되어서 동일성은 더이상 같지 않다.
    System.out.println(&quot;name reference same : &quot;+ (person.getName() == clonePerson.getName()));

    //3. 가변 객체(List)의 경우 해당 객체의 레퍼런스를 복사한다.
    System.out.println(&quot;person hobbies reference : &quot;+(person.getHobbies() == clonePerson.getHobbies()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djDN0c/dJMcaaXTBvS/Zwg5iWE7BNoHIfkdjxec5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djDN0c/dJMcaaXTBvS/Zwg5iWE7BNoHIfkdjxec5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djDN0c/dJMcaaXTBvS/Zwg5iWE7BNoHIfkdjxec5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjDN0c%2FdJMcaaXTBvS%2FZwg5iWE7BNoHIfkdjxec5k%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;850&quot; height=&quot;219&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려면 clone을 재정의할 때 hobbies에 대해 새로 ArrayList를 생성하고 내부의 값들도 새로 생성해서 넣어줘야 한다. 이를 보통 &lt;b&gt;깊은 복사&lt;/b&gt;라고 말한다.&lt;/p&gt;
&lt;pre id=&quot;code_1762233892954&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cloned.addresses = new ArrayList&amp;lt;&amp;gt;();
for (Address addr : this.addresses) {
    cloned.addresses.add(new Address(addr.city));  // 각 요소도 복사
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 clone 메서드를 사용할 때, 불변 객체와 가변객체인지에 따라 얕은 복사만으로 충분한지 판단할 수 있고, 왜 불변 객체를 사용할 수 있다면 사용해야 하는지 알 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 문제의 상황 : 원시형 2차원 배열 clone()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩 테스트 문제를&amp;nbsp; 풀며 경험했던 상황은 2차원 원시형 배열을 clone()할 때였다. 이펙티브 자바에서 배열은 clone 기능을 제대로 사용하는 유일한 예라고 말한다. 아래와 같이 1차원 원시형 배열일 경우 clone()을 호출하면 새로운 배열 객체가 생성되고 모든 값이 복사되는 깊은 복사로 동작한다.&lt;/p&gt;
&lt;pre id=&quot;code_1762235232772&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Music implements Cloneable{
    public int [] arr = {1,2,3,4,5};
    public int [][] arr2 = {
            {1,2,3},
            {4,5,6}
    };
}

// 실행 코드
Music music = new Music();
Music clone = music.clone();
System.out.println(&quot;arr:       &quot; + System.identityHashCode(music.arr));
System.out.println(&quot;cloned:    &quot; + System.identityHashCode(clone.arr));

clone.arr[0] = 100;
System.out.println(clone.arr[0] + &quot; &quot; + music.arr[0]);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3eMVz/dJMcac2tOB8/cPocdMQcpPMCJWe7Xipux0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3eMVz/dJMcac2tOB8/cPocdMQcpPMCJWe7Xipux0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3eMVz/dJMcac2tOB8/cPocdMQcpPMCJWe7Xipux0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3eMVz%2FdJMcac2tOB8%2FcPocdMQcpPMCJWe7Xipux0%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;850&quot; height=&quot;220&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;205&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 내부 스택과 힙 구조를 간략하게 보면 아래와 같다. 1차원 배열은 새로운 배열 객체가 생성되고 모든 값이 복사되므로, clone 배열을 수정해도 원본 music 배열에 영향을 주지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1029&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9gHtQ/dJMcaeTwRM2/RKsCMIJsFWcilDBaz8BnKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9gHtQ/dJMcaeTwRM2/RKsCMIJsFWcilDBaz8BnKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9gHtQ/dJMcaeTwRM2/RKsCMIJsFWcilDBaz8BnKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9gHtQ%2FdJMcaeTwRM2%2FRKsCMIJsFWcilDBaz8BnKk%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;850&quot; height=&quot;374&quot; data-origin-width=&quot;1029&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 2차원 원시형 배열에서는 어떻게 동작할까?&lt;/p&gt;
&lt;pre id=&quot;code_1762237085235&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int[][] original = {{10, 20}, {30, 40}};
int[][] cloned = original.clone();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1161&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6KLfU/dJMcajHiM19/Dfgwom3Kp2DTnAFc2YN7s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6KLfU/dJMcajHiM19/Dfgwom3Kp2DTnAFc2YN7s0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6KLfU/dJMcajHiM19/Dfgwom3Kp2DTnAFc2YN7s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6KLfU%2FdJMcajHiM19%2FDfgwom3Kp2DTnAFc2YN7s0%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;850&quot; height=&quot;293&quot; data-origin-width=&quot;1161&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차원 배열의 메모리 구조를 이해하면 답을 알 수 있다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2차원 배열 변수는 실제로 &quot;1차원 배열들의 배열&quot;이다&lt;/li&gt;
&lt;li&gt;최상위 배열의 각 요소는 내부 1차원 배열에 대한 &lt;b&gt;참조값&lt;/b&gt;을 저장한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clone()을 호출하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;최상위 1차원 배열은 새로 생성된다&lt;/li&gt;
&lt;li&gt;하지만 내부 배열들은 복사되지 않고 &lt;b&gt;참조만 복사&lt;/b&gt;된다 (얕은 복사)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 cloned와 original의 최상위 배열은 다르지만, 내부 배열들은 동일한 객체를 가리키게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 동작하기 때문에, 코딩 테스트 문제 풀이에서 cloned를 변경할 때 original에도 영향이 가는 상황이 발생했던 것이다. 이를 해결하려면 위에서 가변객체를 위한 clone을 재정의했던 방식처럼 동일하게 복사 로직을 작성해줘야 한다. 배열 변수가 가리키는 1차원 배열의 경우 새로 생성된다는 점을 이용해 복사를 아래처럼 작성하면 서로 영향을 주는 상황을 해결할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1762237806980&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int [][] original = {{10, 20}, {30, 40}};
int [][] clone  = new int[2][];

for(int i = 0; i&amp;lt;2;i++){
    clone[i] = original[i].clone();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, clone()을 안전하게 사용하려면 다음을 확인해야 한다:&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;: 복사하려는 객체가 가변 객체인가?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복사 깊이 결정&lt;/b&gt;: 내부 요소까지 복사가 필요한가?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적절한 구현&lt;/b&gt;: 필요하다면 내부 요소를 직접 복사하는 로직 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 원시형 2차원 배열이나 ArrayList 같은 가변 객체는 clone()만으로는 내부 요소의 참조만 복사된다. 원본을 보존해야 하는 경우, 반드시 내부 요소까지 값을 복사하는 로직을 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이펙티브 자바에서는 복사 생성자와 복사 팩토리라는 더 나은 객체 방식을 제공하는 것이 Best Practice라고 소개한다. 따라서, 이미 Cloneable을 구현한 게 아니라면 복사 생성자를 고려해 보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&lt;b&gt;코딩테스트에서 적용&lt;/b&gt;`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩테스트에서는 원시형 2차원 배열과 ArrayList를 자주 사용한다. 원본 데이터를 유지하면서 변경을 가해야 하는 상황에서는 아래와 같이 사용하자.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 2차원 배열 - 각 행을 개별적으로 복사
int[][] cloned = new int[original.length][];
for (int i = 0; i &amp;lt; original.length; i++) {
    cloned[i] = original[i].clone();
}

// ArrayList - 새 객체 생성 후 깊은 복사
List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; cloned = new ArrayList&amp;lt;&amp;gt;();
for (List&amp;lt;Integer&amp;gt; list : original) {
    cloned.add(new ArrayList&amp;lt;&amp;gt;(list));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&lt;b&gt;배운 점&lt;/b&gt;`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clone() 메서드를 깊이 있게 공부하면서 그동안 명확히 설명하지 못했던 개념들을 정리할 수 있는 시간이었다.&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;불변 객체 vs 가변 객체&lt;/b&gt;의 차이와 clone() 시 주의사항&lt;/li&gt;
&lt;li&gt;Java에서&lt;b&gt; 얕은 복사 vs 깊은 복사&lt;/b&gt;의 개념&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 구조&lt;/b&gt;를 통한 배열 복사의 동작 원리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>Clone</category>
      <category>java</category>
      <category>object</category>
      <category>[LG유플러스] 유레카 백엔드</category>
      <category>가변 객체</category>
      <category>멀티캠퍼스부트캠프</category>
      <category>부트캠프후기</category>
      <category>불변 객체</category>
      <category>이펙티브 자바</category>
      <author>chan_2</author>
      <guid isPermaLink="true">https://hechan2.tistory.com/31</guid>
      <comments>https://hechan2.tistory.com/31#entry31comment</comments>
      <pubDate>Tue, 4 Nov 2025 16:01:40 +0900</pubDate>
    </item>
    <item>
      <title>디자인 패턴 : 빌더 &amp;amp; 싱글톤 패턴 구현해보기 (with. 이펙티브 자바)</title>
      <link>https://hechan2.tistory.com/30</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot로 개발하다 보면 @Builder, @Service같은 어노테이션을 너무나 자연스럽게 사용하게 됩니다. 하지만 문득 이런 생각이 들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lRw8k/dJMcaeFXDF0/mwKaIuSKY9vvZoBDETpXB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lRw8k/dJMcaeFXDF0/mwKaIuSKY9vvZoBDETpXB0/img.png&quot; data-alt=&quot;Builder 사용 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lRw8k/dJMcaeFXDF0/mwKaIuSKY9vvZoBDETpXB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlRw8k%2FdJMcaeFXDF0%2FmwKaIuSKY9vvZoBDETpXB0%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;604&quot; height=&quot;264&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Builder 사용 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&quot;만약 Lombok이나 Spring이 없다면 어떻게 구현해야 할까?&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에는 &lt;b&gt;빌더 패턴&lt;/b&gt;과 &lt;b&gt;싱글톤 패턴&lt;/b&gt;을 직접 구현하면서 이전까지 이론적으로만 학습해왔던 디자인 패턴들에 대해 자세하게 공부해 보며 직접 적용할 때는 어떤 점을 주의해야하는지 학습해 보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 빌더 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 필요할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 생성할 때 이런 코드를 본 적 있으신가요?&lt;/p&gt;
&lt;pre id=&quot;code_1761629269889&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Pizza pizza = new Pizza(12, &quot;thin&quot;, &quot;mozzarella&quot;, &quot;tomato&quot;, &quot;olive&quot;, &quot;pepperoni&quot;, true, false);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LsBBw/dJMcaelESw8/y39klM8t1ig9VP6kxGQXT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LsBBw/dJMcaelESw8/y39klM8t1ig9VP6kxGQXT1/img.png&quot; data-alt=&quot;Pizza 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LsBBw/dJMcaelESw8/y39klM8t1ig9VP6kxGQXT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLsBBw%2FdJMcaelESw8%2Fy39klM8t1ig9VP6kxGQXT1%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;477&quot; height=&quot;339&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;339&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pizza 사진&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;매개변수가 8개나 되는데, 순서도 헷갈리고 어떤 값이 무엇을 의미하는지 전혀 알 수 없습니다. (물론 인텔리제이 IDE가 친절하게 매개변수 명을 보여주긴 합니다.) 게다가 일부 매개변수는 선택적이어서 필요 없는 값까지 null이나 false로 채워야 하는 상황이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌더 패턴은 바로 이런 문제를 해결합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761629337802&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Pizza pizza = Pizza.builder()
    .size(12)
    .crust(&quot;thin&quot;)
    .cheese(&quot;mozzarella&quot;)
    .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훨씬 읽기 쉽고, 필요한 옵션만 선택할 수 있습니다. 이것이 바로 &lt;b&gt;클라이언트 코드의 가독성을 보호&lt;/b&gt;하는 빌더 패턴의 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직접 구현해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 빌더 패턴을 Lombok을 사용해 작성해왔기 때문에 어떤점을 고려해서 작성해야하는지 &lt;b&gt;이펙티브 자바의 item 2&lt;/b&gt;을 참고했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`1. 정적 멤버 클래스로 구현하기`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &quot;왜 굳이 static을 붙여야 하지?&quot;라는 의문이 들었습니다. 하지만 일반 내부 클래스로 구현하면 아래와 같은 상황이 발생합니다&lt;/p&gt;
&lt;pre id=&quot;code_1761629895867&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 일반 내부 클래스의 문제점
PizzaV2 pizza = new PizzaV2(12, &quot;thin&quot;); // 먼저 Pizza를 생성해야 함
PizzaV2.Builder builder = pizza.new Builder(12, &quot;thin&quot;); // Builder를 사용하기 위해 Pizza가 필요한 상황&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pizza를 만들려고 Builder를 쓰는데, Builder를 쓰려면 Pizza가 필요한 순환 논리에 빠지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 정적 멤버 클래스를 사용하면 외부 클래스의 인스턴스 없이도 Builder에 접근할 수 있고 이를 통해 아래와 같이 Pizza를 생성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;69&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIMEQe/dJMcai2FdCs/QIsY9RpklCBWRBDbI6E9S1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIMEQe/dJMcai2FdCs/QIsY9RpklCBWRBDbI6E9S1/img.png&quot; data-alt=&quot;Builder 사용 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIMEQe/dJMcai2FdCs/QIsY9RpklCBWRBDbI6E9S1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIMEQe%2FdJMcai2FdCs%2FQIsY9RpklCBWRBDbI6E9S1%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;565&quot; height=&quot;69&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;69&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Builder 사용 사진&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;`2. 필수 필드와 선택 필드 구분하기`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 Builder를 구현하게 되는 경우 해당 클래스에서 필수적으로 전달받아야하는 필드가 있고 선택사항이 되는 필드가 존재합니다. 아래는 제가 직접 실습을 진행할 때 작성해보았던 간단한 예제 클래스입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761630147600&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Pizza {
    private final int size;
    private final String dough;
    private final boolean cheese;
    private final boolean peperoni;
    private final boolean mushroom;
    private final boolean onion;
    private final boolean olive;

    private Pizza(int size, String dough, boolean cheese, boolean peperoni, boolean mushroom, boolean onion, boolean olive) {
        this.size = size;
        this.dough = dough;
        this.cheese = cheese;
        this.peperoni = peperoni;
        this.mushroom = mushroom;
        this.onion = onion;
        this.olive = olive;
    }

    @Override
    public String toString() {
        return &quot;practice_builder.Pizza{&quot; +
                &quot;size=&quot; + size +
                &quot;, dough='&quot; + dough + '\'' +
                &quot;, cheese=&quot; + cheese +
                &quot;, peperoni=&quot; + peperoni +
                &quot;, mushroom=&quot; + mushroom +
                &quot;, onion=&quot; + onion +
                &quot;, olive=&quot; + olive +
                '}';
    }

    public static class Builder{
        private final int size;
        private final String dough;
        private boolean cheese = false;
        private boolean peperoni = false;
        private boolean mushroom = false;
        private boolean onion = false;
        private boolean olive = false;

        public Builder(int size, String dough) {
            this.size = size;
            this.dough = dough;
        }

        public Builder cheese() {
            this.cheese = true;
            return this;
        }

        public Builder peperoni() {
            this.peperoni = true;
            return this;
        }

        public Builder mushroom() {
            this.mushroom = true;
            return this;
        }

        public Builder onion() {
            this.onion = true;
            return this;
        }

        public Builder olive() {
            this.olive = true;
            return this;
        }

        public Pizza build() {
            return new Pizza(size, dough, cheese, peperoni, mushroom, onion, olive);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필수 필드의 경우 Builder의 생성자에서 매개변수로 받아 final로 선언하며, 선택필드의 경우 메서드 체이닝으로 기본값을 수정하는 방식으로 진행합니다. 이를 통해 실제 Client에서는 메서드 체이닝을 통해 가독성있게 Pizza 객체를 생성해 사용할 수 있습니다.&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;직접 구현해보니... 빌더 패턴의 경우 보일러 플레이트 코드의 끝판왕이라고 느꼈습니다. Lombok의 `@Builder`가 얼마나 고마운 존재인지 깨달을 수 있는 시간이었습니다. 또, 이렇게 내부 구조를 이해하고 나니 빌더 패턴을 언제 사용해야 하는지, 어떤 방식으로 설계해야 하는지 명확히 알게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n8iGT/dJMcadmKx25/HT6rgD7fofeFpRxfkVAqpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n8iGT/dJMcadmKx25/HT6rgD7fofeFpRxfkVAqpk/img.png&quot; data-alt=&quot;Lombok 최고&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n8iGT/dJMcadmKx25/HT6rgD7fofeFpRxfkVAqpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn8iGT%2FdJMcadmKx25%2FHT6rgD7fofeFpRxfkVAqpk%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;608&quot; height=&quot;254&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Lombok 최고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 싱글턴 패턴&lt;/h2&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;Spring Boot를 사용하다 보면 Service나 Repository를 주입받아 사용합니다. 이때 Spring은 기본적으로 &lt;b&gt;싱글턴 방식&lt;/b&gt;으로 빈(Bean)을 관리합니다. 이때, 만약 매번 새로운 객체를 생성한다면?&lt;/p&gt;
&lt;pre id=&quot;code_1761630590142&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 비효율적인 방식
@GetMapping(&quot;/users&quot;)
public List&amp;lt;User&amp;gt; getUsers() {
    UserService userService = new UserService(); // 매번 생성
    return userService.findAll();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w0c8h/dJMcag4QDet/tIK8EAziAlVKqBPq94KcuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w0c8h/dJMcag4QDet/tIK8EAziAlVKqBPq94KcuK/img.png&quot; data-alt=&quot;인스턴스 N개 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w0c8h/dJMcag4QDet/tIK8EAziAlVKqBPq94KcuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw0c8h%2FdJMcag4QDet%2FtIK8EAziAlVKqBPq94KcuK%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;457&quot; height=&quot;274&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인스턴스 N개 생성&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;API 호출마다 새로운 Service 객체를 생성하고, Service는 또 새로운 Repository를 생성하고... &lt;b&gt;자원 낭비&lt;/b&gt;가 발생합니다. 각각의 Service들은 모두 동일한 로직을 갖고 있어 이렇게 매번 새로 생성해서 사용하는 것은 아무리 봐도 비효율적이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;싱글턴 패턴은 애플리케이션 전체에서 단 하나의 인스턴스만 생성하여 자원을 효율적으로 관리&lt;/b&gt;합니다. (자원을 보호해주는 디자인 패턴)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직접 구현해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤의 경우에도 이펙티브 자바의 item 3를 참고해 구현을 진행해보았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`1.public static final 방식`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;첫 번째 방식은 매우 간단한 방식입니다. 그냥 private으로 생성자를 막고 `Ingredient.INSTANCE`를 이용해 사용하는 방법입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761632094717&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Ingredient {
    public static final Ingredient INSTANCE = new Ingredient();
    
    private Ingredient() {
        // private 생성자로 외부 생성 차단
    }
}

// 사용
Ingredient ingredient = Ingredient.INSTANCE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매우 간단하고 실제 API를 통해 싱글턴임을 명확히 드러낼 수 있다는 장점이 있습니다. 하지만, 기획의 변경에 따라 더 이상 싱글턴을 유지하지 않아도 되는 상황이 발생하면 Ingredient 코드 뿐만 아니라 `Ingredient.INSTANCE`를 사용하는 클라이언트 코드도 변경해야 한다는 단점이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 처럼 여러 클래스에서 싱글턴 인스턴스를 사용하고 있는 상황이었다면, 해당 클래스 모두 코드 수정을 해야한다는 단점이 존재합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddbO1X/dJMcaihh73b/GyKdoMOecOhYMNHo0U5zP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddbO1X/dJMcaihh73b/GyKdoMOecOhYMNHo0U5zP1/img.png&quot; data-alt=&quot;변경에 취약&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddbO1X/dJMcaihh73b/GyKdoMOecOhYMNHo0U5zP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddbO1X%2FdJMcaihh73b%2FGyKdoMOecOhYMNHo0U5zP1%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;488&quot; height=&quot;368&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;468&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;`2. 정적 팩토리 메서드 방식`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 방식은 인스턴스 필드를 private으로 감추고 정적 팩토리 메서드로 해당 인스턴스에 대한 접근 API를 제공해주는 방식입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761632443608&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class IngredientV2 {
    private static final IngredientV2 INSTANCE = new IngredientV2();
    
    private IngredientV2() {}
    
    public static IngredientV2 getInstance() {
        return INSTANCE;
    }
}

// 사용
IngredientV2 ingredient = IngredientV2.getInstance();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 API 변경 없이 싱글턴이 아니게 변경 가능하다는 점입니다. 필드를 그대로 가져다 사용하는 경우와 비교해보면 아래와 같이 코드를 작성해 정적 메서드를 가져다 사용하는 다른 클래스 코드에는 영향을 주지않고 변경 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;229&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bprLAb/dJMcaeFXEBW/ItYedn40SuqXwz6QRXKr70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bprLAb/dJMcaeFXEBW/ItYedn40SuqXwz6QRXKr70/img.png&quot; data-alt=&quot;변경에 유연&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bprLAb/dJMcaeFXEBW/ItYedn40SuqXwz6QRXKr70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbprLAb%2FdJMcaeFXEBW%2FItYedn40SuqXwz6QRXKr70%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;682&quot; height=&quot;229&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;229&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;이 장점 이외에도 제네릭 싱글턴 팩터리, Supplier로 사용 가능하다는데... 저는 아직 한번도 이렇게 사용해 본적이 없어 잘 와닿지 않았습니다. 그래서 일단은 다른 객체와 결합도를 낮출 수 있다는 장점만 챙겨가려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`3. Enum 방식`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 방법은 Enum을 사용하는 방식입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761632736071&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum IngredientV3 {
    INSTANCE;
    
    public void doSomething() {
        System.out.println(&quot;Doing something...&quot;);
    }
}

// 사용
IngredientV3.INSTANCE.doSomething();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이펙티브 자바에서도 Enum을 이용해 싱글턴을 구현하는 것을 적극 추천하는데 그 이유는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 간결하고 추가 코드 없이 싱글톤이 보장됩니다.&lt;/li&gt;
&lt;li&gt;직렬화 자동처리로 직렬화 -&amp;gt; 역직렬화 이후에도 싱글톤이 보장됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한가지 단점이 존재하는데, 바로 상속이 필요한 경우 사용할 수 없다는 점입니다. 하지만 엄청 간단하고 역직렬화시에도 싱글턴을 자동으로 보장해준다는 점에서 확실히 매력적인 방법이라고 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;그럼 1,2번 방식은 직렬화 시에 어떻게 동작할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 직렬화했다가 역직렬화를 진행한다면 역직렬화를 진행하는 실제 동작에서는 이 객체가 싱글턴으로 사용되고 있다는 사실을 모릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 아래와 같이 코드를 작성해 비교하면 싱글턴이 깨지는 상황을 확인해 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761633011449&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Elvis elvis1 = Elvis.getInstance()
System.out.println(&quot;elvis1: &quot; + elvis1);
        
// 직렬화 (객체를 바이트로 변환)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(elvis1);
oos.close();

byte[] serializedData = baos.toByteArray();


// 역직렬화 (바이트를 객체로 복원)
ByteArrayInputStream bais = new ByteArrayInputStream(serializedData);
ObjectInputStream ois = new ObjectInputStream(bais);
Elvis elvis2 = (Elvis) ois.readObject();
ois.close();


System.out.println(&quot;같은 인스턴스? &quot; + (elvis1 == elvis2));  // false!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMO9JR/dJMcagRjrEb/Q5UP1djMMkdlDDUuzEnqWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMO9JR/dJMcagRjrEb/Q5UP1djMMkdlDDUuzEnqWK/img.png&quot; data-alt=&quot;역직렬화 문제&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMO9JR/dJMcagRjrEb/Q5UP1djMMkdlDDUuzEnqWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMO9JR%2FdJMcagRjrEb%2FQ5UP1djMMkdlDDUuzEnqWK%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;690&quot; height=&quot;209&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;209&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;이를 해결하는 방법은 readResolve 메서드를 구현하는 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761633398872&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Ingredient implements Serializable {
    public static final Ingredient INSTANCE = new Ingredient();
    
    private Ingredient() {}
    
    // 역직렬화 시 호출되어 싱글톤 보장
    private Object readResolve() {
        return INSTANCE;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼, 역직렬화의 동작과정중 아래와 같이 메서드를 불러와 실행시키는 부분이 존재하고 이를&amp;nbsp; 통해 싱글턴 객체를 보장할 수 있게됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761633611479&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Method readResolve = Elvis.class.getDeclaredMethod(&quot;readResolve&quot;);
if (readResolve != null) {
    readResolve.setAccessible(true);

    // readResolve() 호출
    Object result = readResolve.invoke(tempInstance);  // INSTANCE 반환

    return (Elvis) result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글턴 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글턴의 경우 직접 구현할 일이 있다면 최우선적으로 Enum을 고려하고, 만약 해당 클래스를 상속해야하는 일이 생긴다면 public 필드나 정적 팩토리 메서드를 사용해야겠다. 결합도를 생각한다면 코드 몇줄만 추가하면 되는 정적 팩토리 메서드를 아마 선택하는게 현명하지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 1번과 2번 방법을 사용하기로 결정하였다면 역직렬화시 발생할 수 있는 싱글턴이 깨지는 상황을 고려해 readResolve() 메서드를 작성해 놓아야 겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 마치며..&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum 싱글턴은 아직 실제로 사용해본 적이 없어 조금 낯설게 느껴지긴 했는데, 나중에 직접 사용할 일이 생긴다면 무조건 Enum을 이용해 싱글턴 패턴을 사용해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 개념 학습에만 초점을 두었던 디자인 패턴 공부였고, 이번에 처음으로 &lt;b&gt;직접 구현하며 주의해야할 문제점들을 다루어보았는데, &lt;/b&gt;생각보다 유익한 시간이었습니다&lt;b&gt;. &lt;/b&gt;다른 디자인 패턴들도 한번 이렇게 직접 구현해보며 학습하고, 최종적으로는 진행하는 프로젝트에 적용해볼 수 있으면 좋을 것 같습니다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>java</category>
      <category>[LG유플러스] 유레카 백엔드</category>
      <category>디자인 패턴</category>
      <category>멀티캠퍼스부트캠프</category>
      <category>부트캠프후기</category>
      <category>빌더 패턴</category>
      <category>싱글턴 패턴</category>
      <category>이펙티브 자바</category>
      <author>chan_2</author>
      <guid isPermaLink="true">https://hechan2.tistory.com/30</guid>
      <comments>https://hechan2.tistory.com/30#entry30comment</comments>
      <pubDate>Tue, 28 Oct 2025 15:59:06 +0900</pubDate>
    </item>
    <item>
      <title>파일 업로드가 귀찮아서 만든 북마크 데스크톱 애플리케이션 회고</title>
      <link>https://hechan2.tistory.com/29</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 기획 동기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;SE-3889a10d-e4d1-454f-9564-f261934d7de6.png&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;925&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgzGrQ/dJMb862m9Y7/KgIvUQUqr1kbZab0G5NbFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgzGrQ/dJMb862m9Y7/KgIvUQUqr1kbZab0G5NbFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgzGrQ/dJMb862m9Y7/KgIvUQUqr1kbZab0G5NbFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgzGrQ%2FdJMb862m9Y7%2FKgIvUQUqr1kbZab0G5NbFk%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;539&quot; height=&quot;338&quot; data-filename=&quot;SE-3889a10d-e4d1-454f-9564-f261934d7de6.png&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;925&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;경로&amp;nbsp;북마크'&amp;nbsp;프로젝트가&amp;nbsp;시작되었다. &lt;br /&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;같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 요구사항 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션&amp;nbsp;개발에&amp;nbsp;앞서&amp;nbsp;가장&amp;nbsp;고민했던&amp;nbsp;지점은&amp;nbsp;&lt;b&gt;&quot;북마크를&amp;nbsp;정말&amp;nbsp;필요한&amp;nbsp;순간에&amp;nbsp;바로&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있어야&amp;nbsp;한다&quot;&lt;/b&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;프로그램을&amp;nbsp;활용하는&amp;nbsp;것이&amp;nbsp;가장&amp;nbsp;효과적일&amp;nbsp;것&amp;nbsp;같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;`파일 업로드 프로세스 분석`&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 파일 업로드 프로세스를 분석했다. 대부분의 웹사이트에서 파일 업로드 버튼을 클릭하면 &amp;lt;input type=&quot;file&quot;&amp;gt; 태그가 동작한다는 것을 확인할 수 있었고 이를 바탕으로&amp;nbsp;크롬 확장 프로그램으로 사용자의 파일 업로드 클릭을 감지하고, 이를 트리거로 내 애플리케이션에 메시지를 전달하는 방식으로 구현 방향을 잡았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;729&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/soUwY/dJMb9OnaUUp/4nTiVEhJfrKTsKEV5CHv0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/soUwY/dJMb9OnaUUp/4nTiVEhJfrKTsKEV5CHv0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/soUwY/dJMb9OnaUUp/4nTiVEhJfrKTsKEV5CHv0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsoUwY%2FdJMb9OnaUUp%2F4nTiVEhJfrKTsKEV5CHv0k%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;624&quot; height=&quot;222&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;729&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;`기술 스택 선택`&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할&amp;nbsp;언어와&amp;nbsp;라이브러리는&amp;nbsp;고민&amp;nbsp;없이&amp;nbsp;결정했다.&amp;nbsp;Java로&amp;nbsp;프로젝트를&amp;nbsp;만들고&amp;nbsp;싶었고,&amp;nbsp;Java에서&amp;nbsp;UI를&amp;nbsp;구현하는&amp;nbsp;대표적인&amp;nbsp;라이브러리인&amp;nbsp;Swing&amp;nbsp;UI를&amp;nbsp;선택했다.&amp;nbsp;따라서&amp;nbsp;메인&amp;nbsp;기술&amp;nbsp;스택은&amp;nbsp;Java&amp;nbsp;+&amp;nbsp;Swing&amp;nbsp;UI로&amp;nbsp;확정되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;`핵심 요구사항`&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적인 방향성과 사용할 도구를 정한 후, 고객의 요구사항을 뽑아보았다. (물론 여기서 고객은 나 자신이었다. ㅋㅋ)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 사용자는 파일, 폴더의 경로를 북마크에 저장한다. &lt;br /&gt;&amp;nbsp; &amp;nbsp; a. 북마크를 저장할 때 이름을 설정할 수 있다. &lt;br /&gt;&amp;nbsp; &amp;nbsp; b. 북마크는 사용용도에 따라 그룹으로 분류할 수 있다. &lt;br /&gt;&amp;nbsp; &amp;nbsp; c. 그룹 내에 경로들은 카테고리로 분류할 수 있다. &lt;br /&gt;2. 사용자는 저장한 북마크들을 볼 수 있으며 잘못 저장한 북마크는 수정하거나 삭제할 수 있다. &lt;br /&gt;&amp;nbsp; &amp;nbsp; a. 그룹과 그룹 사이에 북마크를 옮길 수 있다.&lt;br /&gt;3. 사용자는 저장한 북마크에 대한 파일경로를 복사해갈 수 있다. &lt;br /&gt;4. 사용자는 북마크 리스트에 보여지는 그룹, 카테고리, 북마크 순서를 원하는 순서로 조정할 수 있다. &lt;br /&gt;5. 그룹과 관련해서 CRUD를 진행할 수 있다. &lt;br /&gt;6. 카테고리와 관련해서 CRUD를 진행할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에 필요한 기능을 정리한 결과, 6가지 핵심 요구사항을 도출했다. 이를 바탕으로 프로젝트의 방향성을 다음과 같이 설정했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크롬 확장 프로그램을 만들어 애플리케이션과 연결한다&lt;/li&gt;
&lt;li&gt;Java와 Swing UI 라이브러리를 이용해 데스크톱 애플리케이션을 만든다&lt;/li&gt;
&lt;li&gt;애플리케이션은 정의한 요구사항을 만족하도록 구현한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;`POC 검증 : 크롬 확장 프로그램과 애플리케이션 연동 가능성`&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장&amp;nbsp;먼저&amp;nbsp;검증한&amp;nbsp;것은&amp;nbsp;&lt;b&gt;&quot;크롬&amp;nbsp;확장&amp;nbsp;프로그램과&amp;nbsp;내&amp;nbsp;애플리케이션을&amp;nbsp;연결할&amp;nbsp;수&amp;nbsp;있는가?&quot;&lt;/b&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;점이었다. &lt;br /&gt;&lt;br /&gt;하지만&amp;nbsp;우리에게는&amp;nbsp;생성형&amp;nbsp;AI라는&amp;nbsp;든든한&amp;nbsp;도구가&amp;nbsp;있었기에&amp;nbsp;주저하지&amp;nbsp;않고&amp;nbsp;개발에&amp;nbsp;들어갔다.&amp;nbsp;AI로부터&amp;nbsp;크롬&amp;nbsp;확장&amp;nbsp;프로그램&amp;nbsp;코드를&amp;nbsp;받아&amp;nbsp;구현하되,&amp;nbsp;manifest.json의&amp;nbsp;각&amp;nbsp;키&amp;nbsp;값은&amp;nbsp;Chrome&amp;nbsp;공식&amp;nbsp;문서를&amp;nbsp;참고하며&amp;nbsp;필요한&amp;nbsp;기능만&amp;nbsp;빠르게&amp;nbsp;학습했다.&lt;/p&gt;
&lt;figure id=&quot;og_1761034612286&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;Chrome 확장 프로그램 &amp;nbsp;|&amp;nbsp; Chrome Extensions &amp;nbsp;|&amp;nbsp; Chrome for Developers&quot; data-og-description=&quot;Chrome 확장 프로그램 개발 방법을 알아보세요.&quot; data-og-host=&quot;developer.chrome.com&quot; data-og-source-url=&quot;https://developer.chrome.com/docs/extensions?hl=ko&quot; data-og-url=&quot;https://developer.chrome.com/docs/extensions?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.chrome.com/docs/extensions?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.chrome.com/docs/extensions?hl=ko&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;Chrome 확장 프로그램 &amp;nbsp;|&amp;nbsp; Chrome Extensions &amp;nbsp;|&amp;nbsp; Chrome for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Chrome 확장 프로그램 개발 방법을 알아보세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.chrome.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;과정에서&amp;nbsp;크롬&amp;nbsp;확장&amp;nbsp;프로그램은&amp;nbsp;JavaScript에&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;GPT가&amp;nbsp;생성한&amp;nbsp;간단한&amp;nbsp;Swing&amp;nbsp;UI&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 과정에서 핵심은 Native Messaging Host 등록이었다. 내가 만든 애플리케이션을 크롬 브라우저와 통신할 수 있는 메시지 호스트로 등록하면, 크롬 확장 프로그램에서 발생한 이벤트(파일 업로드 버튼 클릭)를 감지하고 네이티브 애플리케이션으로 메시지를 전달할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pxF9h/dJMb9NV6zW9/RqhEsshobKCRbQT3AsIC2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pxF9h/dJMb9NV6zW9/RqhEsshobKCRbQT3AsIC2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pxF9h/dJMb9NV6zW9/RqhEsshobKCRbQT3AsIC2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpxF9h%2FdJMb9NV6zW9%2FRqhEsshobKCRbQT3AsIC2k%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;769&quot; height=&quot;279&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;`Java 객체지향 프로그래밍과 디자인 패턴`&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결이 가능하다는 것을 확인한 후에는 정리한 요구사항을 토대로 코드를 구현했다. 아래 다이어그램은 최종 결과물의 도메인 모델이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;1006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3Dkuz/dJMb9MXb1ST/HhEzTTMpmhuidnEL33MWd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3Dkuz/dJMb9MXb1ST/HhEzTTMpmhuidnEL33MWd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3Dkuz/dJMb9MXb1ST/HhEzTTMpmhuidnEL33MWd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3Dkuz%2FdJMb9MXb1ST%2FHhEzTTMpmhuidnEL33MWd0%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;1844&quot; height=&quot;1006&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;1006&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 작성할 때 2가지를 중점적으로 고려했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 1. 도메인 모델 단순화 및 설계 개선&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기&amp;nbsp;기획에는&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;b&gt;&amp;nbsp;&quot;카테고리와&amp;nbsp;그룹의&amp;nbsp;차이가&amp;nbsp;뭐지?&quot;&lt;/b&gt;라는&amp;nbsp;혼란을&amp;nbsp;야기할&amp;nbsp;수&amp;nbsp;있다는&amp;nbsp;생각이&amp;nbsp;들었다. &lt;br /&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;구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;객체&amp;nbsp;간&amp;nbsp;협력&amp;nbsp;구조를&amp;nbsp;분석하던&amp;nbsp;중,&amp;nbsp;&quot;그룹&amp;nbsp;생성&quot;&amp;nbsp;기능에서&amp;nbsp;&quot;그룹&amp;nbsp;객체&amp;nbsp;생성&quot;과&amp;nbsp;&quot;생성된&amp;nbsp;그룹을&amp;nbsp;파일에&amp;nbsp;저장&quot;이라는&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;로직을&amp;nbsp;조율하는&amp;nbsp;역할을&amp;nbsp;분리했다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 2. 어댑터 패턴을 통한 유연한 저장소 설계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그룹&amp;nbsp;저장소&amp;nbsp;구현에는&amp;nbsp;MicroStream이라는&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;저장소&amp;nbsp;인터페이스를&amp;nbsp;추상화하고&amp;nbsp;MicroStream을&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이&amp;nbsp;과정을&amp;nbsp;통해&amp;nbsp;8월부터&amp;nbsp;9월까지&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;확신이&amp;nbsp;들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;`파일 입출력과 MicroStream 라이브러리`&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 데이터 영속화 전략: 데이터베이스 vs 파일 시스템&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의&amp;nbsp;프로그램은&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;기존 웹 서비스를 개발할 때는 데이터베이스를 활용했지만, 이 애플리케이션에도 데이터베이스가 필요한가? 고민한 결과 대답은 No였다. 그 이유는 다음과 같다. 가벼운 소프트웨어를 만드는 것이 목적인데, 데이터베이스를 포함하는 것 자체가 무거워진다는 생각이 들었다.또, 데이터베이스를 이용한다고 쳤을 때, 로컬 DB를 사용한다는 것은 나중에 배포를 생각해보면 있을 수 없는 일이었다.&lt;br /&gt;&lt;br /&gt;따라서&amp;nbsp;간단하게&amp;nbsp;파일&amp;nbsp;입출력을&amp;nbsp;이용해&amp;nbsp;데이터를&amp;nbsp;영속화하는&amp;nbsp;것으로&amp;nbsp;결정했다. &lt;br /&gt;처음에는 Java에서 제공하는 기본 파일 입출력 기능을 사용하려고 했다. 하지만 파일 저장 및 조회 시 필요한 직렬화/역직렬화 코드, 데이터 저장 및 수정 시 필요한 영속성 관리 코드를 작성하는 데 드는 비용이 4일 안에 해결하기 어렵다고 판단했다. MVP를 빠르게 완성하기 위해 기존 라이브러리를 활용하기로 결정하고 파일 입출력 라이브러리를 찾아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. MicroStream 선택 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그&amp;nbsp;과정에서&amp;nbsp;MicroStream을&amp;nbsp;선택하게&amp;nbsp;되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;MicroStream은 JVM 메모리상에 존재하는 객체들의 참조 구조를 그대로 파일 시스템에 영속화하는 라이브러리다. 예를 들어, Group 객체가 Bookmark 리스트를 관리하는 경우, Group은 각 Bookmark에 대한 참조를 보유한다. MicroStream은 이러한 객체 그래프 구조를 있는 그대로 저장하고 복원할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 MicroStream을 선택한 이유는 크게 2가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 단순한 도메인 모델과의 최적 호환성&lt;/b&gt;&lt;br /&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;루트(Root)&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;MicroStream과&amp;nbsp;궁합이&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;392&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDW5rb/dJMb9WSX95Z/D6OpEjd8MTBVyuFmF17Tuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDW5rb/dJMb9WSX95Z/D6OpEjd8MTBVyuFmF17Tuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDW5rb/dJMb9WSX95Z/D6OpEjd8MTBVyuFmF17Tuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDW5rb%2FdJMb9WSX95Z%2FD6OpEjd8MTBVyuFmF17Tuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;392&quot; height=&quot;387&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;2) 단일 언어로 통합된 데이터 처리&lt;/b&gt;&lt;br /&gt;Java Stream API만으로 정렬, 필터링, 조회 등 모든 데이터 처리 로직을 구현할 수 있다는 점이 큰 장점이었다. SQL 기반 파일 저장 방식을 사용했다면 Java 코드 외에 SQL 쿼리까지 관리해야 하는 복잡도가 추가된다. 만약 MicroStream을 활용한다면? 데이터 저장부터 조회까지 순수 Java 코드만으로 일관되게 처리할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L2txg/dJMb9NIztI4/JtQiUjfDTXEOCcLdbCYLm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L2txg/dJMb9NIztI4/JtQiUjfDTXEOCcLdbCYLm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L2txg/dJMb9NIztI4/JtQiUjfDTXEOCcLdbCYLm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL2txg%2FdJMb9NIztI4%2FJtQiUjfDTXEOCcLdbCYLm0%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;632&quot; height=&quot;279&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;물론 처음 사용하는 라이브러리인 만큼 동시성 문제나 메모리 관리 이슈가 발생할 가능성도 생각했다. 하지만 앞서 적용한 어댑터 패턴 덕분에 실제 사용 중 문제가 발견되더라도 저장소 구현체만 교체하면 되는 구조이기 때문에, 리스크를 충분히 관리 가능한 수준으로 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;`Swing UI를 영리하게 작성하기 (With AI Agent)`&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는&amp;nbsp;GPT를&amp;nbsp;이용해&amp;nbsp;단순하게&amp;nbsp;내가&amp;nbsp;작성한&amp;nbsp;서비스&amp;nbsp;로직과&amp;nbsp;연결하는&amp;nbsp;방식으로&amp;nbsp;UI를&amp;nbsp;구현했다. &lt;br /&gt;아래와&amp;nbsp;같은&amp;nbsp;프롬프트를&amp;nbsp;활용해&amp;nbsp;Swing&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;2~3번&amp;nbsp;반복&amp;nbsp;요청하지&amp;nbsp;않을&amp;nbsp;수&amp;nbsp;있다는&amp;nbsp;사실을&amp;nbsp;배웠다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 다음 내부 기능들을 구현했는데 Swing UI 연결을 부탁할게. MainFrame 코드는 전달해 줄게. &lt;br /&gt;- 북마크 리스트를 보여줄 때 BookmarkType을 보고 파일 아이콘, 폴더 아이콘 붙여서 보여주기 bookmark.getType()으로 타입 확인 가능 &lt;br /&gt;- 북마크 그룹 삭제 연결 &amp;rarr; 이 메서드 활용: bookmarkGroupService.deleteBookmarkGroup(long id); &lt;br /&gt;- 북마크 그룹 이름 변경 연결 &amp;rarr; 이 메서드 활용: bookmarkGroupService.renameBookmarkGroup(long id, String name); &lt;br /&gt;- 북마크 순서 변경 연결(북마크 그룹과 동일하게 드래그 앤 드롭 방식) &amp;rarr; 이 메서드 활용: reorderBookmark(long groupId, long prevId, int toIndex) &lt;br /&gt;- 북마크 정보 수정 연결 &amp;rarr; 이 메서드 활용: updateBookmark(long bookmarkId, String displayName, String path)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbJHus/dJMb9Qeejpy/23DF6bOgzTzaoqKxsynAS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbJHus/dJMb9Qeejpy/23DF6bOgzTzaoqKxsynAS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbJHus/dJMb9Qeejpy/23DF6bOgzTzaoqKxsynAS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbJHus%2FdJMb9Qeejpy%2F23DF6bOgzTzaoqKxsynAS0%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;260&quot; height=&quot;341&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과, 위 사진의 UI가 탄생했다. (아직 좀 볼품없습니다~)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. AI Agent로 UI 개선 자동화 시도&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이후&amp;nbsp;UI&amp;nbsp;개선&amp;nbsp;작업을&amp;nbsp;진행하면서&amp;nbsp;GPT에게&amp;nbsp;&quot;분석&amp;nbsp;요청&amp;nbsp;&amp;rarr;&amp;nbsp;수정&amp;nbsp;요청&quot;을&amp;nbsp;반복적으로&amp;nbsp;진행했다.&amp;nbsp;이&amp;nbsp;과정에서&amp;nbsp;&quot;내가&amp;nbsp;매번&amp;nbsp;이&amp;nbsp;루틴을&amp;nbsp;조율해야&amp;nbsp;할&amp;nbsp;필요가&amp;nbsp;있을까?&quot;라는&amp;nbsp;의문이&amp;nbsp;들었고,&amp;nbsp;이를&amp;nbsp;AI로&amp;nbsp;자동화할&amp;nbsp;수&amp;nbsp;있지&amp;nbsp;않을까&amp;nbsp;하는&amp;nbsp;아이디어가&amp;nbsp;떠올랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그래서&amp;nbsp;이번&amp;nbsp;기회에&amp;nbsp;AI&amp;nbsp;Agent를&amp;nbsp;실제로&amp;nbsp;구현해보기로&amp;nbsp;결심하고&amp;nbsp;바로&amp;nbsp;Claude&amp;nbsp;Code&amp;nbsp;구독을&amp;nbsp;결제했다.&amp;nbsp;반복적인&amp;nbsp;UI&amp;nbsp;개선&amp;nbsp;프로세스를&amp;nbsp;자동으로&amp;nbsp;수행할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;Agent를&amp;nbsp;쉘&amp;nbsp;스크립트로&amp;nbsp;구현하는&amp;nbsp;것을&amp;nbsp;목표로&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;261&quot; data-origin-height=&quot;165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/welK3/dJMb89SkwEp/AtSTT5R24fppFKNihRzQdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/welK3/dJMb89SkwEp/AtSTT5R24fppFKNihRzQdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/welK3/dJMb89SkwEp/AtSTT5R24fppFKNihRzQdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwelK3%2FdJMb89SkwEp%2FAtSTT5R24fppFKNihRzQdK%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;261&quot; height=&quot;165&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;165&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와&amp;nbsp;같은&amp;nbsp;구조로&amp;nbsp;동작하는&amp;nbsp;Agent를&amp;nbsp;만들었지만,&amp;nbsp;아쉽게도&amp;nbsp;직접&amp;nbsp;사용해볼&amp;nbsp;수는&amp;nbsp;없었다.&amp;nbsp;내가&amp;nbsp;만든&amp;nbsp;Agent는&amp;nbsp;Claude&amp;nbsp;Code가&amp;nbsp;아니라&amp;nbsp;Claude&amp;nbsp;API를&amp;nbsp;사용할&amp;nbsp;때만&amp;nbsp;작동했고,&amp;nbsp;API&amp;nbsp;비용&amp;nbsp;문제로&amp;nbsp;Claude&amp;nbsp;Code를&amp;nbsp;구독한&amp;nbsp;상황이었기&amp;nbsp;때문에&amp;nbsp;다른&amp;nbsp;방법을&amp;nbsp;찾아야&amp;nbsp;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 반자동화 워크플로우 구축&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전한&amp;nbsp;자동화는&amp;nbsp;아니지만,&amp;nbsp;대안으로&amp;nbsp;장기&amp;nbsp;작업용&amp;nbsp;프롬프트를&amp;nbsp;설계하는&amp;nbsp;방식을&amp;nbsp;선택했다.&amp;nbsp;Agent에서&amp;nbsp;설계했던&amp;nbsp;작업&amp;nbsp;순서를&amp;nbsp;프롬프트로&amp;nbsp;그대로&amp;nbsp;옮기고,&amp;nbsp;마지막에&amp;nbsp;&quot;이&amp;nbsp;과정을&amp;nbsp;N번&amp;nbsp;반복해줘&quot;라는&amp;nbsp;지시를&amp;nbsp;추가했다. &lt;br /&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;있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKFpaJ/dJMb9aX0i4s/mKhJmmqvCZSqygTdujEvHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKFpaJ/dJMb9aX0i4s/mKhJmmqvCZSqygTdujEvHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKFpaJ/dJMb9aX0i4s/mKhJmmqvCZSqygTdujEvHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKFpaJ%2FdJMb9aX0i4s%2FmKhJmmqvCZSqygTdujEvHK%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;293&quot; height=&quot;491&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;982&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 반자동화 방식도 완벽하지는 않았다. 프롬프트에 세세한 디자인 지침까지 모두 담지 못했기 때문에, 처음부터 만족하는 수준의 UI가 완성되지는 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;UI의 핵심 구조와 뼈대&lt;/b&gt;는 효과적으로 구축할 수 있었고, 세부적인 스타일링과 레이아웃 조정은 추가 프롬프팅을 통해 점진적으로 개선해 나갔다. 이러한 반복 과정을 거쳐 최종적으로 아래와 같은 UI를 완성했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;1003&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2L8El/dJMb9PM9Qad/m60xdcjrBLpmvvyYyMAK9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2L8El/dJMb9PM9Qad/m60xdcjrBLpmvvyYyMAK9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2L8El/dJMb9PM9Qad/m60xdcjrBLpmvvyYyMAK9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2L8El%2FdJMb9PM9Qad%2Fm60xdcjrBLpmvvyYyMAK9K%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;301&quot; height=&quot;525&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;1003&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 앞으로 Swing UI를 이용하여 애플리케이션을 만들 일이 생긴다면 내가 직접 Swing 을 자세하게 공부하고 코드를 짜기보다 Swing UI로 만들 수 있는 UI 명칭을 공부하고 AI에게 명칭을 통해 내가 원하는 구조를 명확하게 표현하는게 더 좋은 UI를 탄생시킬 수 있을 것 같다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 시연 영상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 단은 정말 걱정을 많이했는데 이번 기회에 생성형 AI의 위대함을 한번 더 경험해볼 수 있었던 것 같다. 아래 영상은 20초 정도 되는 영상인데 앞으로 내가 파일 관리나 URL 관리를 할 때 애용할 것 같다.&lt;/p&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;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;파일 탐색기에 해당 경로를 붙여넣기 후 확인을 누른다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상에서는 너무 빠르게 지나가 뭔 차이지 싶을 수도 있는데 파일 탐색기가 켜지면서 내 애플리케이션 우상단에 같이 올라오는 것이 차이점이다. 해당 북마크를 통해 자주 쓰거나 원하는 파일을 바로 찾아 업로드 할 수 있는 것이 가장 큰 장점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 개선해야 될 점들..&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정을 거쳐 최종적으로 동작하는 소프트웨어를 완성했다. 하지만 4일이라는 시간 안에 구현했기 때문에 부족한 부분도 있고 추가하지 못한 기능도 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 내용을 정리해본 결과 크게 3가지 범주로 분류할 수 있었다. 직접 사용해보며 겪은 경험을 바탕으로 뽑아낸 것이라 이것 외에도 더 존재한다고 생각한다. 따라서 이후에는 아래 작업을 진행하며 애플리케이션 고도화를 진행할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개선작업&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;- Swing UI 코드 분리 및 각 컴포넌트의 이름 정리하기 (ex. 트레이, 팝업 메뉴, 툴바 등)
- 애플리케이션 z-index 수정하기 &amp;rarr; 현재는 가장 앞에만 표시됨
- Swing UI에서 보여주는 메시지가 애플리케이션 하단에 표시되는데 가독성이 떨어짐
- Main 코드 수정 및 설정 관련 코드 분리하기
- 북마크 &amp;harr; 크롬 확장 프로그램 통신 설계하기&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;b&gt;추가기능 작업&lt;/b&gt;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;- 검색 기능
- lnk로 바로가기 만들어 유저가 파일/폴더 위치를 이동시켜도 문제없이 동작하도록 구현 (JNA)
- 그룹, 북마크 순서 변경 UI 추가하기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가 인프라 작업&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 스토어 개발자 등록 후 크롬 확장 프로그램 등록하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1993&quot; data-origin-height=&quot;1027&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw70rE/dJMb83EzPgq/QnvGT9x5j8XcL6KmDeoPX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw70rE/dJMb83EzPgq/QnvGT9x5j8XcL6KmDeoPX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw70rE/dJMb83EzPgq/QnvGT9x5j8XcL6KmDeoPX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw70rE%2FdJMb83EzPgq%2FQnvGT9x5j8XcL6KmDeoPX1%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;716&quot; height=&quot;369&quot; data-origin-width=&quot;1993&quot; data-origin-height=&quot;1027&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 최종 목표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성된 파일 경로 북마크 v1은 지금 실제로 사용하고 있다. 사용해보니 크롬에서 자주 사용하는 파일을 업로드하는 과정이 정말 빨라진 것을 체감할 수 있었다. &lt;br /&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;그렇게&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가&amp;nbsp;불편했던&amp;nbsp;것을&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;/p&gt;</description>
      <category>프로젝트</category>
      <category>java</category>
      <category>MicroStream</category>
      <category>Swing</category>
      <category>[LG유플러스] 유레카 백엔드</category>
      <category>개인프로젝트</category>
      <category>객체지향</category>
      <category>멀티캠퍼스IT부트캠프</category>
      <category>부트캠프후기</category>
      <category>북마크</category>
      <category>어댑터패턴</category>
      <author>chan_2</author>
      <guid isPermaLink="true">https://hechan2.tistory.com/29</guid>
      <comments>https://hechan2.tistory.com/29#entry29comment</comments>
      <pubDate>Tue, 21 Oct 2025 20:55:51 +0900</pubDate>
    </item>
  </channel>
</rss>