<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>DS log</title>
    <link>https://rasony.tistory.com/</link>
    <description>살아남기 위해 배우고, 배우기 위해 기록합니다</description>
    <language>ko</language>
    <pubDate>Wed, 15 Jul 2026 08:53:27 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>DS log</managingEditor>
    <image>
      <title>DS log</title>
      <url>https://tistory1.daumcdn.net/tistory/5073698/attach/9b7385d072674d4f991166e83c90a6d2</url>
      <link>https://rasony.tistory.com</link>
    </image>
    <item>
      <title>1년차 스타트업 백엔드 개발자 회고</title>
      <link>https://rasony.tistory.com/227</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;오늘은 연차다. 처음 회고를 작성해봐서 어떻게 작성해야할 지 모르겠지만, 무의식의 흐름대로 써보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 지금 내 삶에 가장 큰 영향을 주고 있는 회사에 대해서 작성해보려고 한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;회사 사정이 좋지 않다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로다. 회사 사정이 좋지 않다. 회사 내에서는 여러가지 이유로 희망 퇴직 및 권고 사직을 받고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 내가 진행하던 솔루션이 회사의 주 사업 중 하나였는데 해당 사업이 실패하면서 동시에 내 커리어도 멈추게 되었다.&amp;nbsp;&lt;br /&gt;뭐 그렇다고 아직 회사를 잘리지는 않았으나 뭐 어떻게 될런 지 모르겠다.&amp;nbsp;&lt;br /&gt;자세한 이야기를 말할 수는 없지만 내가 어떻게 할 수는 없는 것이니깐 버티는 수밖에 없을 듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;회사에서 무엇을 얻었나?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1년 조금 넘게 초기 솔루션을 개발하고, 운영하면서 얻은 것은 많지만 지금 생각나는 것은..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔루션에 문제가 가지 않는 선에서 내가 적용해보고 싶은 것을 다 해봤다.&lt;br /&gt;&lt;br /&gt;특정 기술을 선택하는데 있어서 팀 내에서 적합하다고 판단이 들면 바로 적용을 할 수 있어서 좋았다. 최근에 내가 공부한 게 바로 실무에서 적용을 할 수 있으니 솔직히 재미있었다.&lt;br /&gt;&lt;br /&gt;내가 낸 의견이 다른 사람에게는 &lt;b&gt;야근의 시작&lt;/b&gt;일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회의 시간에 내가 프로덕트가 이런 식으로 발전했으면 좋겠다. 뭐 이런 이야기를 한 적이 있다. 그때 당시에는 몰랐지만 나중에 생각해보니 이는 누군가에게는 또 다른 일의 시작이다.&lt;br /&gt;단순히, 프로덕트가 발전하면 좋은 거 아닌가? 그런가. 그럴 수도 있는데 지금 내 생각은 좀 현실에 찌들은 거 같다. 프로덕트를 개발하는 입장에서 프로덕트에 애정이 있는 것은 당연하다. 그렇지만, 세상이 썩 단순하지는 않은 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일은 &lt;b&gt;예상치 못하게&lt;/b&gt; 들어온다.&lt;br /&gt;&lt;br /&gt;솔루션을 개발하다보니 특정 회사에 맞게 솔루션을 커스텀해야하는 일이 잦았다. 다행히 프로덕트를 초기부터 기획하고 개발해왔기 때문에 어떤 포인트를 이렇게 변경하면 여러가지로 확장이 가능하다. 뭐 이런 것들을 알게되었다.&amp;nbsp;&lt;br /&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;p data-ke-size=&quot;size16&quot;&gt;특정 의견에 반대할 때는 &lt;b&gt;다른 대안&lt;/b&gt;을 가져와야한다.&lt;br /&gt;&lt;br /&gt;1년차 개발자임에도 불구하고, 백엔드 개발을 혼자하다보니 내가 가능하다. 혹은 불가능하다. 라고 말한 것들이 의사결정에 반영이 되는 경우가 있었다. 특히, 기획자분들과 이야기를 많이 하였는데 항상 이런 식이다.&lt;br /&gt;&lt;br /&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;br /&gt;그리고, 보통 바로 결정은 하지 못 한다. 검토해볼 시간이 필요하다. 라고 말한 후 해당 목적을 달성할 수 있는 더 쉬운 방법, 혹은 가능한 방법 등 여러 대안을 만든다.&lt;br /&gt;&lt;br /&gt;그리고, 기획자분께 여러 대안들을 가지고 가져간다. 이제 내가 할 수 있는 일은 끝났다. 결정이 나기를 기다릴 뿐...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 운이 좋게 어느 정도 의견이 반영됬던 거 같다. 앞으로 어떻게 될 지 모르겠지만 앞으로 내가 잘 하기를 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것 외에 여러가지를 경험했고, 많은 것들을 알게 되었지만 쓰다보면 끝도 없을 거 같아서 이만..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;항해 99 10기 활동&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 일하면서 백엔드 개발을 혼자 하다보니 개발적으로 의견 교환을 하는 게 쉽지 않았다. 제일 많이 대화한 친구는 Gemini 일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼, 개발적으로 실력 향상을 위해서 이것저것 찾아보다보니 항해 99라는 것을 발견하게 되었고. 참가 비용이 꽤나 부담이 되었지만 커리큘럼이 너무 마음에 들어서 못 먹어도 Go 하고 수강을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항해 99는 주니어 개발자를 대상으로 하는 교육이고, 나는 항해 99 백엔드 플러스 과정을 수강했다.&lt;br /&gt;&lt;br /&gt;10주차동안 활동을 진행을 했는데 활동하면서 개발적으로 몰입을 많이 할 수 있는 시간이었다. 퇴근 후에 특정 과제를 해결하기 위해서 몰입하고 연구하는 시간은 나에게 값진 경험이었고. 내 개발적 시야를 한 단계 높일 수 있는 시간이었다.&lt;br /&gt;&lt;br /&gt;10기 과정이 끝난 후에 '항해상'이라는 것을 받으면서 시원섭섭하게 마무리했다.&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;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k3ZDH/dJMcahrvdsM/VgSGZiAOnWWtzkohKyKZb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k3ZDH/dJMcahrvdsM/VgSGZiAOnWWtzkohKyKZb0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;1230&quot; data-filename=&quot;스크린샷 2026-07-02 오전 9.56.10.png&quot; width=&quot;500&quot; height=&quot;816&quot; style=&quot;width: 45.7198%; margin-right: 10px;&quot; data-widthpercent=&quot;46.26&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k3ZDH/dJMcahrvdsM/VgSGZiAOnWWtzkohKyKZb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk3ZDH%2FdJMcahrvdsM%2FVgSGZiAOnWWtzkohKyKZb0%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;754&quot; height=&quot;1230&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxJ8gn/dJMcabxYasg/mpoIDkLdQqGnSxIbeTyRKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxJ8gn/dJMcabxYasg/mpoIDkLdQqGnSxIbeTyRKK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;1230&quot; data-filename=&quot;스크린샷 2026-07-02 오전 9.55.53.png&quot; width=&quot;316&quot; height=&quot;444&quot; style=&quot;width: 53.1174%;&quot; data-widthpercent=&quot;53.74&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxJ8gn/dJMcabxYasg/mpoIDkLdQqGnSxIbeTyRKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxJ8gn%2FdJMcabxYasg%2FmpoIDkLdQqGnSxIbeTyRKK%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;876&quot; height=&quot;1230&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 시간은 학습할 때 타이머를 눌러야하는데 하나도 누르지 않아 기본 시간만 반영이 됬다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;사이드 프로젝트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 사이드 프로젝트 2개를 하고 있다. 1개는 아는 지인분들과 진행 중이고 다른 하나는 '넥스터즈'라는 동아리에서 진행 중이다.&lt;br /&gt;&lt;br /&gt;지인분들과 하는 프로젝트는 오더런이라는 프로젝트인데 Python / FastAPI로 진행 중이다. 해당 기술을 선택한 이유는 내가 다룰 수 있는 기술의 폭을 넓히고 싶었고. 회사에서도 사용하고 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넥스터즈에서 하는 프로젝트의 경우 현재 팀빌딩 기간이라서 아직 정해지지는 않았지만 아마도 Java or Kotlin / SpringBoot를 사용하지 않을까 싶다. 최근에 Python을 많이 보다보니 이것도 Python으로 할 수 있으면 좋겠는데.. 한 번 어떤 팀에 속하고 어떤 프로젝트인지 봐야할 듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;앞으로 나는?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞으로 나는 무엇을 해야 할까?&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개발자로 먹고 살 수는 있을까? 직무를 바꿔야할까? 직무를 바꾼다면 뭐로 바꿀 수 있을까? 내가 좋아하는 것은 뭘까? 내가 할 수 있는 것은 뭘까?&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이런 질문을 스스로에게 계속 하고 있는 상황이다. 슬프게도 모든 질문에 대한 답은 '모두 개발을 하고 싶다.' 이다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하고 싶은 것을 하고 모두 하고 살수는 없겠지만,, 아직 도전해볼 수 있는 나이라고 생각하기에 개발을 계속 해볼 생각이다. 구체적인 기간은 정하지 않았지만 할 수 있는데까지는 해봐야지.&lt;br /&gt;&lt;br /&gt;예전에는 무조건 Java와 Spring만 파고들어야 한다고 생각했고, 채용 공고도 Java 기반 포지션 위주로 찾아봤다. 하지만 지금은 시야를 조금 넓혀 Python까지 학습 영역을 확장하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음 목표는 AI 백엔드 개발자다. 지금까지 쌓아온 실무 경험과 기술 스택, 그리고 현재 주니어 개발자 채용 시장을 고려했을 때 나의 경쟁력은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Java/Spring&lt;/span&gt;&lt;/b&gt;&lt;span&gt;과 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Python/FastAPI + AI&lt;/span&gt;&lt;/b&gt;&lt;span&gt;의 조합이라고 생각한다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞으로는 이 두 가지 무기를 바탕으로 더 넓은 기회를 향해 도전해보려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;</description>
      <category>회고</category>
      <category>1년차개발자</category>
      <category>개발자성장기</category>
      <category>개발자회고</category>
      <category>사이드프로젝트</category>
      <category>이직준비</category>
      <category>주니어개발자</category>
      <category>회고록</category>
      <author>DS log</author>
      <guid isPermaLink="true">https://rasony.tistory.com/227</guid>
      <comments>https://rasony.tistory.com/227#entry227comment</comments>
      <pubDate>Thu, 2 Jul 2026 10:16:12 +0900</pubDate>
    </item>
    <item>
      <title>외부 API 연동 환경에서 안정적인 통계 집계 배치 개발하기</title>
      <link>https://rasony.tistory.com/226</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;작년에 '외부 서비스 데이터를 연동해서 자사 서비스에 최적화된 통계 화면을 제공해!' 라는 요구사항을 받았습니다.&lt;br /&gt;&lt;br /&gt;이번 포스팅에서는 해당 요구사항을 해결하면서 고민했던 부분을 정리해보려고 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요구사항 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 요구사항은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;2시간 주기로 최신화된 데이터를 제공하라.&lt;/li&gt;
&lt;li&gt;기간별로 통계 데이터를 조회가 가능해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 개발자로서 고려해야하는 상황은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;외부 API 응답 속도는 통제할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1차 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항을 받고 대략적으로 설계를 해봤을 때 처음 든 생각은 내부 서비스에 API 요청을 하면 요청 시점에 외부 API를 호출해 응답을 가공한 뒤 반환하는 동기 처리 방식이었습니다.&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;'당장 오늘 점심까지 해라' 와 같이 시간 제한이 타이트하지 않기도 하고, 자사 서비스의 스레드가 외부 API의 응답을 기다리면서 블라킹되어 스레드 고갈 문제가 발생할 수도 있기 때문에 바로 기각했습니다.&lt;br /&gt;&lt;br /&gt;응답 시간, 기획 변경에 따른 영향 등을 고려할 때 '배치 기반 수집/적재 구조'로 설계하기로 결정했습니다.&lt;br /&gt;&lt;br /&gt;-&amp;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-filename=&quot;스크린샷 2026-04-08 오후 6.05.32.png&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KlcFl/dJMcacbCbCz/SH3nkNCHMB8HxsIX99jkV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KlcFl/dJMcacbCbCz/SH3nkNCHMB8HxsIX99jkV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KlcFl/dJMcacbCbCz/SH3nkNCHMB8HxsIX99jkV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKlcFl%2FdJMcacbCbCz%2FSH3nkNCHMB8HxsIX99jkV0%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;1114&quot; height=&quot;898&quot; data-filename=&quot;스크린샷 2026-04-08 오후 6.05.32.png&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;상세 설계를 하며 고민한 것들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적인 설계 방향을 정한 뒤 상세 설계를 진행하였습니다. 아래는 설계를 진행하면서 고민한 내용입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 데이터 저장 모델 선택&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 고민한 것은 데이터 저장 방식이었습니다. &lt;br /&gt;&lt;br /&gt;가공된 데이터를 DB에 저장 할 지 vs 준원본 데이터를 저장할 지&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전자의 경우에는 통계 API에서 추가적인 가공 없이 바로 사용할 수 있어 응답 속도가 빠르다는 장점이 있습니다. 그러나, 기획 변경에 유연하게 대응하기가 힘듭니다.&lt;br /&gt;&lt;br /&gt;후자의 경우에는 추가적인 가공이 필요하지만 기획 변경에는 유연하게 대응이 가능합니다.&lt;br /&gt;&lt;br /&gt;저는 후자 방식을 선택을 했는데 그 이유는 기획이 변경될 가능성이 크다고 생각을 했고, API 응답 속도가 문제가 있을 경우에는 기존에 저장된 원본 데이터를 사용해서 테이블을 하나 만들어서 데이터를 미리 가공하여 적재해놓으면 된다고 생각했습니다.&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;2. 적절한 청크 사이즈 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API는 원하는 정보를 1번부터 N번까지 원하는 것을 한 번에 호출할 수 있는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 자사 서비스에 맞게 청크 사이즈를 조절할 수 있었고, 테스트를 통해서 대략적인 값을 설정해놓고, 이후 운영을 하면서 조금씩 조절해나가기로 하였습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 병렬 처리&lt;/h4&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;사용자 규모가 작기 때문에 처리 시간이 오래 걸리지도 않았기 때문에 굳이 병렬 처리는 하지 않았습니다. 추후에 배치 수행 시간이 오래 걸리면 그때 병렬 처리를 도입하기로 결정하였습니다.&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;4. 트랜잭션 및 DB 커넥션 관리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크 단위를 정한 다음에는 저장 구간의 트랜잭션 경계를 어디에 둘지 결정해야 했습니다. 외부 API 호출과 DB 저장을 같은 트랜잭션으로 묶으면 커넥션 점유 시간이 길어집니다. 외부 응답 지연이 DB 자원 문제로 번질 수 있다는 점이 가장 큰 리스크였습니다.&lt;/p&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;5. 분산 환경 대응&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 인스턴스 환경에서는 같은 배치가 동시에 실행될 수 있습니다. 이 경우 중복 수집과 저장 충돌이 발생할 수 있어 별도 제어가 필요했습니다.&amp;nbsp; 그래서 ShedLock을 선택해 동시 실행을 제어했습니다. 핵심 이유는 두 가지였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 별도 인프라 없이 기존 DB 기반으로 분산 락을 운영할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, lockAtMostFor와 lockAtLeastFor를 통해 비정상 종료나 중복 실행 상황에서 좀비 커넥션 문제가 길어지는 것을 방지할 수 있었습니다.&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;6. 재시도/복구 전략&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API 호출 실패를 단순 로그로 끝내면 운영 복구가 어렵습니다. 그래서 집계 대상 정보를 먼저 DB에 저장하고, 상태 기반으로 처리 흐름을 추적하는 방식을 고민했습니다.&lt;/p&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;br /&gt;&lt;br /&gt;외부 API 호출 전에 저장될 통계에 대해서 대기 상태를 부여하고, 집계가 완료되어 데이터가 성공적으로 저장되면 성공 상태로 변경하는 것입니다. 그리고, 모종의 이유로 집계가 실패하였으면 실패 상태로 변경했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조로 진행하게 되면, 실패 대상을 식별해 재처리할 수 있고, 운영자가 진행 상태를 확인할 수 있으며, 장애가 발생해도 필요한 대상만 선택적으로 복구할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;회고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 요구사항을 개발하면서 외부 API 응답 결과를 기반으로 어떻게 하면 안정적으로 배치 처리를 수행할 수 있을까에 대해서 고민할 수 있었습니다.&lt;/p&gt;</description>
      <category>트러블 슈팅</category>
      <category>동시성</category>
      <category>배치</category>
      <category>외부API연동</category>
      <category>재시도</category>
      <category>트랜잭션관리</category>
      <author>DS log</author>
      <guid isPermaLink="true">https://rasony.tistory.com/226</guid>
      <comments>https://rasony.tistory.com/226#entry226comment</comments>
      <pubDate>Wed, 8 Apr 2026 18:46:27 +0900</pubDate>
    </item>
    <item>
      <title>[Redis] 내부 아키텍처에 대해서 알아보자.</title>
      <link>https://rasony.tistory.com/225</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;기존에 Redis하면 자동 반사로 특징은 싱글 스레드! 라고 암기가 되어있었는데요. 이번에 좋은 기회로 Redis를 공부하게 되서 오늘은 Redis 내부 아키텍처에 대해서 소개해보려고 합니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메인 스레드와 보조 스레드&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 메인 스레드 1개와 백그라운드 보조 스레드들로 구성되어 있습니다. 간단하게 주요 작업은 메인 스레드가 처리하고 시간이 오래걸리는 작업은 백그라운드 보조 스레드가 처리하는 구조입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 메인 스레드&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메인 스레드는 클라이언트의 모든 명령어를 순차적으로 처리하는 핵심 엔진입니다. 이때 이벤트 루프 기반의 비동기 I/O를 활용하여 단일 스레드임에도 불구하고 높은 처리량을 보여줍니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 보조 스레드&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보조 스레드들은 메인 스레드가 처리하기에 시간이 오래 걸리는 I/O 작업을 전담합니다. 예를 들어, 대용량 데이터 삭제를 위한 UNLINK 명령어, RDB 파일 저장을 위한 BGSAVE, 또는 AOF 파일 재작성 등이 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 작업 큐를 통한 협업&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 스레드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;작업 큐&lt;/b&gt;를 통해 협업합니다. 메인 스레드가 처리하기에 오래 걸리는 작업을 발견하면, 그 작업을 작업 큐에 넣어 보조 스레드에게 위임합니다. 보조 스레드는 이 큐에서 작업을 꺼내 수행합니다. 덕분에 메인 스레드는 작업을 큐에 넣는 아주 짧은 시간 외에는 클라이언트의 명령어를 막힘 없이 수행할 수 있게 됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;싱글 스레드인데 어째서 빠르다고 할까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 하나의 메인 스레드로 여러 클라이언트와 연결된 소켓들의 요청을 동시에 처리합니다. 이 과정은 이벤트 루프를 중심으로 아래와 같이 진행됩니다.&lt;br /&gt;&lt;br /&gt;1. 이벤트 대기 &lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Redis의 메인 스레드는 I/O 멀티플렉싱 기술(리눅스의 epoll 등)을 사용하여 다수의 클라이언트 소켓들을 감시합니다. 메인 스레드는 소켓에 클라이언트 요청 데이터가 도착하여 '읽기 가능(Readable)' 이벤트가 발생하기를 효율적으로 대기합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 데이터 수신 및 버퍼 저장&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이벤트 루프가 소켓의 읽기 이벤트를 즉시 감지하면, 메인 스레드가 활성화됩니다. 메인 스레드는 소켓으로부터 요청을 읽어 해당 소켓의 입력 버퍼(Input Buffer)에 저장합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 순차 명령어 실행&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터가 버퍼에 저장된 후, 메인 스레드는 이 버퍼에 있는 명령어를 FIFO 방식으로 순차적으로 가져와 실행합니다. 이 실행 과정이 바로 Redis가 싱글 스레드로 원자성을 핵심입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 메인 스레드는 I/O 작업을 기다리며 시간을 낭비하지 않고, 실제 명령어를 처리하는 데에만 집중하여 높은 성능을 유지할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;싱글 스레드 모델의 장단점은 무엇일까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 싱글 스레드 명령어 실행 모델은 다음과 같은 장점과 단점을 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어들이 순차적으로 실행되므로, 복잡한 Lock 메커니즘 없이도 Race Condition이 발생하지 않습니다. 따라서 데이터 정합성을 매우 쉽게 보장할 수 있는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러나, 스레드가 1개라는 점에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Head-of-Line Blocking &lt;/b&gt;이 발생할 위험이 있습니다. 하나의 명령어가 오래 실행되면, 그 작업이 끝날 때까지 다른 모든 클라이언트의 요청 처리가 지연됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이것이 바로 KEYS와 같은 시간 복잡도가 높은 명령어를 사용하지 말고 SCAN 명령어를 사용하라고 하는 이유입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;싱글-스레드인데-multiexec-트랜잭션은-왜-필요할까&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;싱글 스레드인데 MULTI/EXEC 트랜잭션은 왜 필요할까?&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;공부를 하다가 싱글 스레드인데 &quot;왜 Redis에 트랜잭션을 보장해주는 MULTI/EXEC 같은 명령어가 있는 거지?&quot;라는 의문이 들었습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 의문의 답은 Redis가 처리하는 요청의 근본적인 환경을 생각하면 이해하기 쉽습니다. Redis 메인 스레드는 하나지만, 외부 클라이언트로부터 요청을 받는 소켓(연결)은 여러 개가 동시에 존재합니다. Redis의 싱글 스레드는 한 번에 하나의 명령만 실행하므로 단일 명령어의 원자성은 보장합니다. 하지만, 여러 클라이언트가 각각의 소켓을 통해 동시에 요청을 보내면, Redis는 이 소켓들에서 들어온 명령어들을 하나씩 번갈아 순차적으로 처리합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서, 특정 클라이언트 두 개의 명령어를 보냈을 때, 이 두 명령어 사이에 다른 클라이언트 B의 명령어가 끼어들어 실행될 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MULTI/EXEC 트랜잭션은 바로 이 문제를 해결합니다. 트랜잭션 블록 내의 모든 명령어들을 하나의 작업 묶음으로 간주하여, 해당 블록이 실행되는 동안에는 다른 소켓에서 들어온 어떠한 명령도 끼어들지 못하게 막아, 다수 명령어 시퀀스의 원자성을 보장해 줍니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;결론&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 단일 메인 스레드로 명령을 순차적으로 처리하는 싱글 스레드 모델을 채택하여 복잡한 락 없이도 Race Condition 문제를 근본적으로 방지하고 데이터의 원자성을 보장합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기에 I/O 멀티플렉싱(I/O Multiplexing) 기술 기반의 이벤트 루프를 결합하여, I/O 대기 시간을 극복하고 단일 스레드로 수많은 클라이언트의 요청을 동시에 처리합니다. 결국 Redis는 명령어 실행의 단순성과 I/O 처리의 비동기성이라는 두 가지 핵심 요소를 결합하여 안정적인 고성능 처리를 달성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기술</category>
      <category>redis</category>
      <category>Redis아키텍처</category>
      <category>백그라운드스레드</category>
      <category>싱글스레드</category>
      <category>트랜잭션</category>
      <author>DS log</author>
      <guid isPermaLink="true">https://rasony.tistory.com/225</guid>
      <comments>https://rasony.tistory.com/225#entry225comment</comments>
      <pubDate>Mon, 6 Apr 2026 22:40:49 +0900</pubDate>
    </item>
    <item>
      <title>내가 테스트를 작성하며 고민한 것들</title>
      <link>https://rasony.tistory.com/224</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-06 오후 9.15.46.png&quot; data-origin-width=&quot;369&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI1RHy/dJMcaaY8bcq/6rdREKWdQUk11S6VedeYKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI1RHy/dJMcaaY8bcq/6rdREKWdQUk11S6VedeYKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI1RHy/dJMcaaY8bcq/6rdREKWdQUk11S6VedeYKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI1RHy%2FdJMcaaY8bcq%2F6rdREKWdQUk11S6VedeYKk%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;369&quot; height=&quot;385&quot; data-filename=&quot;스크린샷 2026-04-06 오후 9.15.46.png&quot; data-origin-width=&quot;369&quot; data-origin-height=&quot;385&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;요즘 AI를 사용하여 개발할 때 테스트 시나리오를 먼저 생각하는 것은 기본이 된 거 같습니다.&lt;br /&gt;&lt;br /&gt;불과 1년전만 하더라도 테스트 시나리오부터 생각하지 않았는데 말이죠.&lt;br /&gt;&lt;br /&gt;이제는 개발을 할 때 구현할 Spec을 정해놓은 다음에 고려한 테스트 시나리오를 전부 입력한 다음에 테스트 시나리오가 성공할 때&lt;br /&gt;까지 딸깍합니다.&lt;br /&gt;&lt;br /&gt;그러곤, 다음 할 것으로 넘어가서 다음에 무엇을 할 지 생각하곤 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 테스트를 작성해야할까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;왜 테스트를 작성해야할까? 인제는 개발을 AI가 하기 때문에 사실상 제가 할 수 있는 게 테스트 시나리오를 생각하고 구상하는 것뿐입니다.&lt;br /&gt;식상한 이야기지만, 최근에 개발하고 있는 서비스가 대대적으로 개편이 되고 있습니다.&lt;br /&gt;&lt;br /&gt;벌써부터 머리가 아픈데, 기존 API에 대해서 하위 호환성을 지켜야합니다. 하위 호환성 이것을 지키기 위해서라도 테스트를 통해서 특정 구간을 지켜(?)줘야 한다고 생각합니다.&lt;br /&gt;&lt;br /&gt;또, 테스트 자체가 AI에게 하나의 가이드 라인이 되는 것도 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;테스트를 어떻게 작성하는 게 좋을까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 작성하는 방법은 크게 고전파 방식과 런던파 방식이 있습니다.&lt;br /&gt;&lt;br /&gt;런던파는 테스트 대상이 의존하는 모든 객체를 테스트 대역(Mock)으로 대체하여 테스트 단위를 완전히 격리해야한다! 라고 보는 입장입니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;- 테스트 대상 코드를 협력자로부터 완전히 분리
- 단일 클래스의 독립적 테스트 지향
- 객체 간의 협력 구조 자체가 검증 대상&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면, 고전파는 단위 테스트에서 모든 의존성을 대역으로 바꾸는 것을 지양하는 입장입니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;- 공유 상태를 유발하는 외부 의존성(DB, 파일 시스템 등)만 테스트 대역 사용
- 가능한 실제 객체를 사용하며 테스트 간 격리 유지
- 의존성 자체를 교체하기보다 실제 동작의 결과에 집중&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;저는 고전파 방식을 선호하는 편입니다. 어처피 각 Entity에 대해서 Fixture 클래스를 잘 만들어놓고. 테스트 시나리오를 입력해놓으면 AI가 다 만들어주는데 이왕이면 정확히 하는 게 좋지 않을까 합니다.&lt;br /&gt;&lt;br /&gt;다만, 외부 API를 호출하는 경우는 테스트 대역을 사용하는 게 좋습니다.&lt;br /&gt;&lt;br /&gt;이에 대해서 고민한 구글의 사례가 있습니다. 참고해보면 좋을 듯 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;구글은 모의 객체 프레임워크를 만능으로 여겼던 시기를 거쳐, 현재는 이를 최소화하고 실제에 가까운 테스트를 작성하는 방향으로 선회했습니다. Mock 위주의 테스트는 작성하기는 쉽지만 버그를 잡아내는 능력이 떨어지고, 코드 수정 시 테스트도 함께 보수해야 하는 비용이 크다는 교훈을 얻었기 때문입니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;테스트 데이터 어떻게 생성하는 게 좋을까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 개발하는 서비스는 사용자의 상태에 따라 비즈니스 로직이 동적으로 변하는 구조입니다. 따라서 테스트 데이터가 각 케이스마다 존재했어야했습니다. 또한, 문진 데이터가 많기 때문에 고정된 디폴트 데이터가 많았습니다.&lt;br /&gt;&lt;br /&gt;우선, 문진 데이터의 경우 서비스의 핵심이기 때문에 프로젝트 내부에서 Flyway를 사용해서 관리하는 방식을 선택했습니다.&lt;/p&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;동적 데이터의 경우 Fixture Monkey 라이브러리를 사용해서 생성하였습니다. 선택한 이유는 엔티티에 선언된 Validation 관련 애노테이션을 참고해서 그거에 맞게 테스트 데이터를 만들 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;테스트 우선순위&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉽게도 회사에서 Claude Max를 지원해주지 않습니다. 그래서 토큰을 아껴써야하는데요..&amp;nbsp; 그 방법(?) 중에 하나가 테스트 우선순위를 정하는 것입니다.&lt;br /&gt;&lt;br /&gt;모든 레이어를 테스트하면 좋겠지만, 아쉽게도 그럴 여건이 안 됩니다. 그래서 제 나름대로 테스트 우선순위를 정했습니다.&lt;br /&gt;&lt;br /&gt;1순위: API 통합 테스트&lt;/p&gt;
&lt;pre class=&quot;erlang&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;대상: Controller 진입점부터 DB까지의 Happy Path + 주요 예외 케이스.

목표: &quot;이 API가 스펙대로 동작하고 DB에 잘 저장되는가?&quot;를 검증.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2순위: 도메인 단위 테스트&lt;/p&gt;
&lt;pre class=&quot;erlang&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;대상: 복잡한 계산식이나 상태 변경 로직이 담긴 Entity 및 POJO.

목표: 다양한 경계값에서도 비즈니스 로직이 깨지지 않음을 보장.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;레거시 코드 어떻게 다룰 것인가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;테스트 코드 없이 레거시 코드를 다 감수하시겠습니까?&quot;라는 우아한형제들 기술 블로그의 글을 참고해서 실무에 적용중인데 아주 유용합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #12b886;&quot; href=&quot;https://techblog.woowahan.com/2613/&quot;&gt;테스트 코드 없이 레거시 코드를 다 감수하시겠습니까? 우아한형제들 기술 블로그&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 id=&quot;테스트-환경-어떻게-구축할-것인가&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;테스트 환경 어떻게 구축할 것인가?&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 개인 로컬 DB를 사용해서 개발을 진행하고 있었습니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이는 개발자마다 데이터 상태가 달라 테스트 신뢰도를 떨어뜨리는 문제가 있었습니다. 이를 해결하기 위해 두 가지 대안을 검토했고, 최종적으로 Testcontainers를 선택했습니다. 그 이유는 '관리하기 편해서' 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서, 팀원분들과 이야기를 한 후에 테스트 환경을 구축을 하고 하나하나 테스트를 만들어갔습니다. 다행히(?) 테스트가 없는 백지 환경이어서 오히려 편했습니다...&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Docker Compose를 통해 테스트 환경을 구축할 수 있었지만 테스트 전에 수동으로 해당 컨테이너를 실행해줘야하기 때문에 불편했습ㄴ디.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;지금까지 테스트를 작성하며 했던 고민들을 간략하게 정리해봤습니다. 감사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>트러블 슈팅</category>
      <category>AI</category>
      <category>단위테스트</category>
      <category>테스트</category>
      <category>테스트컨테이너</category>
      <category>통합테스트</category>
      <author>DS log</author>
      <guid isPermaLink="true">https://rasony.tistory.com/224</guid>
      <comments>https://rasony.tistory.com/224#entry224comment</comments>
      <pubDate>Mon, 6 Apr 2026 21:40:02 +0900</pubDate>
    </item>
    <item>
      <title>트랜잭션 아웃박스 패턴을 이용하여 데이터 정합성 확보하기 (회원탈퇴)</title>
      <link>https://rasony.tistory.com/223</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;회사에서 회원 탈퇴 기능에 대한 요구사항을 받았습니다.&lt;br /&gt;&lt;br /&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;제거 요구사항은 각 데이터의 성격에 따라서 Hard Delete 처리를 하거나 Soft Delete 처리를 수행해야했습니다. 운이 좋게도?&amp;nbsp; 담당자분들이 모종의 이유로 없었기 때문에 외부 서버의 API까지 개발할 수 있었습니다.&lt;br /&gt;&lt;br /&gt;오늘은 이 요구사항을 처리하면서 고민했던 것들을 정리해보려고 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;요구사항 정리&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;회원 탈퇴 시 해당 사용자와 관련된 데이터를 제거해야한다. (내부 서비스)&lt;/li&gt;
&lt;li&gt;회원 탈퇴 시 해당 사용자와 관련된 데이터를 제거해야한다. (외부 서비스)&lt;/li&gt;
&lt;li&gt;회원 탈퇴 시 해당 사용자와 관련된 이미지를 제거해야한다. (외부 서비스)&lt;/li&gt;
&lt;li&gt;데이터의 성격에 따라 Hard Delete를 수행하거나, Soft Delete를 수행해야한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;어떻게 처리하면 좋을까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리할 수 있는 방법은 다양합니다. 그 중에서 대표적인 케이스를 비교해보면 아래와 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;트랜잭션 안에서 외부 API를 직접 호출합니다.&lt;/li&gt;
&lt;li&gt;트랜잭션 커밋 후 외부 API를 즉시 1회만 호출합니다.&lt;/li&gt;
&lt;li&gt;트랜잭션 아웃박스 패턴을 이용해 비동기로 API를 호출하고, 실패시 배치로 재시도합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;트랜잭션 안에서 외부 API 직접 호출하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'트랜잭션 안에서 외부 API 직접 호출하기' 는 구현이 가장 단순하고 흐름이 직관적인 장점(?) 이 있지만 아래와 같은 단점 때문에 보통 사용하지 않습니다.&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;외부 API 호출이 성공했으나 내부 서비스의 트랜잭션이 롤백이 됬다면?&lt;/li&gt;
&lt;li&gt;내부 서비스의 트랜잭션이 커밋이 됬는데, 외부 API 호출이 유실 됬다면?&lt;/li&gt;
&lt;li&gt;외부 API 응답 지연이 발생해 내부 서비스의 트랜잭션이 길어진다면? (장애 전파)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 외부 API에 대한 응답이 꼭 있어야만 가능한 경우는 사용할 수 있을 듯합니다.&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;size18&quot;&gt;트랜잭션 커밋 후 외부 API를 즉시 1회만 호출하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 방법은 트랜잭션 밖에서 외부 API를 호출함으로써 '트랜잭션 안에서 외부 API를 직접 호출'의 몇몇 단점을 커버했지만, 외부 API 호출이 실패했을 경우에 대한 대책이 없는 한계가 있습니다.&lt;br /&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;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 아웃박스 패턴은 내부 트랜잭션 내에서 일어하는 상태 변경과 외부 API 호출 기록을 같은 트랜잭션 내에 묶는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 외부 API 호출 기록을 저장하기 때문에 실패 시 재시도 하고 최종 실패를 추적할 수 있어 운영 시 복구가 용이하다는 장점이 있습니다.&lt;br /&gt;&lt;br /&gt;그러나, 이 방법은 최종 일관성을 맞추는 구조이고 즉각적인 일관성은 없기 때문에 이에 관해서 어떻게 할 지 충분히 생각을 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그외에도 외부 API 호출 멱등성, 모니터링 등 고려해야할 것이 많습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;트랜잭션 아웃박스 패턴을 채택&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;회원 탈퇴의 기준 상태를 내부 DB에서 먼저 확정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;외부 호출 &quot;의도&quot;를 outbox 테이블에 같은 트랜잭션으로 저장합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;커밋 이후 비동기 1회 시도와 배치 재시도로 결국 처리합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;[하나의 로컬 트랜잭션]&lt;br /&gt;&lt;br /&gt;회원 상태 변경 + outbox INSERT&lt;br /&gt;-&amp;gt; commit&lt;br /&gt;&lt;br /&gt;[커밋 이후]&lt;br /&gt;&lt;br /&gt;즉시 비동기 1회 전송&lt;br /&gt;실패 시 PENDING 유지&lt;br /&gt;배치가 재시도&lt;br /&gt;최종 SUCCESS 또는 최종 FAIL&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;이외에 고민했던 부분들..&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배치로 외부 API 호출을 재시도 해야하는 데 배치의 주기는 어떻게 할까?&lt;br /&gt;&lt;br /&gt;먼저, 수행 시간을 측정하고 추적하면서 배치 주기를 점차 수정해나가야합니다.&lt;br /&gt;Fixed Delay를 사용하지 말고, 지수 백오프를 사용해서 외부 서버의 복구 시간을 확보하는 전략이 좋을 듯 하지 않을까 생각하는데, 맞는 지는모르겠네요.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;외부 API 호출 재시도 실패 시 개발자가 관여해서 수동으로 복구해야하는 시점은 언제일까?&lt;br /&gt;&lt;br /&gt;재시도 횟수를 ?회 정도로 제한해야하지 않을까 싶습니다.&lt;/li&gt;
&lt;li&gt;멱등성 문제는 없을까?&lt;br /&gt;&lt;br /&gt;API를 개발할 때 멱등성 키를 Param으로 받아서 중복된 요청의 경우 실제로 처리가 안 되도록 해야합니다.&lt;br /&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;모니터링은 어떻게 할까?&lt;br /&gt;&lt;br /&gt;Micreometer 등을 활용해 outbox failure count와 같은 커스텀 메트릭을 등록해서 Grafana 대시보드에서 이를 볼 수 있도록 만들어야합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;</description>
      <category>트러블 슈팅</category>
      <category>멱등성</category>
      <category>모니터링</category>
      <category>비동기처리</category>
      <category>외부API</category>
      <category>트랜잭션아웃박스패턴</category>
      <author>DS log</author>
      <guid isPermaLink="true">https://rasony.tistory.com/223</guid>
      <comments>https://rasony.tistory.com/223#entry223comment</comments>
      <pubDate>Sun, 5 Apr 2026 15:21:57 +0900</pubDate>
    </item>
    <item>
      <title>계약마다 다른 쿼터 정책을 어떻게 설계할까</title>
      <link>https://rasony.tistory.com/222</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 회사에서 개발하면서 겪었던, 저에게 꽤 의미 있던 경험을 정리해보려고 합니다. 저는 B2B 솔루션을 개발하는 팀에 속해 있습니다. 이번에 그 솔루션에 계약마다 다른 쿼터 정책을 적용해야 하는 기능을 개발하게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 CRUD 개발이 아니라서 확실히 흥미롭긴 했습니다. 그런데, '계약에 관한 정책이 위에서 정해지지 않았는데, 내 마감기한은 그대로네 이대로라면 야근 확정이네 ~ ' 하는 자포자기의 심정도 들었습니다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;기능마다 서로 다른 쿼터 정책이 필요하고, 그 정책은 계약에 따라 달라진다.&amp;nbsp;&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;처음에는 &quot;기능마다 if-else로 나누면 되지 않을까?&quot;라고 생각했습니다. 그런데 조금만 더 생각해 보니 이 방식은 금방 한계가 보였습니다. 정책이 늘어날수록 서비스 메서드는 비대해지고, 동시 요청이 들어오기 시작하면 검증과 차감 로직이 여기저기 흩어질 가능성이 컸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 비기능 요구사항도 있었습니다. 자원 사용량이 계약에 정해진 범위를 정확히 지켜야 한다는 점입니다. 결국 동시성 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 그 문제를 어떻게 바라봤고, 왜 &lt;code&gt;정책 분리&lt;/code&gt;와 &lt;code&gt;원자적 갱신&lt;/code&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;처음에는 쿼터 시스템을 단순히 &quot;남은 횟수를 확인하고 차감하는 기능&quot; 정도로 생각했습니다. 하지만 실제로 어려웠던 것은 차감 자체가 아니라, &lt;b&gt;정책의 기준이 고정돼 있지 않다&lt;/b&gt;는 점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 식입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능 A는 사용자 기준으로만 제한한다.&lt;/li&gt;
&lt;li&gt;기능 B는 조직 기준으로만 제한한다.&lt;/li&gt;
&lt;li&gt;기능 C는 사용자와 조직 조건을 동시에 만족해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &quot;사용량을 추적한다&quot;는 행위는 같지만, &lt;b&gt;어떻게 검증할지&lt;/b&gt;는 계약에 따라 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황에서 가장 피하고 싶었던 것은 정책 분기문이 비즈니스 로직 전체로 퍼지는 구조였습니다. 새로운 계약 타입이 추가될 때마다 핵심 서비스 코드에 조건문을 덧붙이는 방식은, 처음에는 빠를 수 있어도 결국 유지보수 비용이 커질 수밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 초반에 세운 기준은 하나였습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;바뀌는 것은 정책이고, 바뀌지 않는 것은 &quot;사용량을 검증하고 기록한다&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정책을 동적으로 선택할 수 있게 하자.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 택한 방식은 정책별로 사용량 처리 전략을 분리하는 것이었습니다. 핵심 아이디어는 단순합니다. 또 전략패턴입니다.. ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스는 &quot;이번 요청에 어떤 정책을 적용해야 하는지&quot;만 결정한다.&lt;/li&gt;
&lt;li&gt;실제 검증과 차감 방식은 정책별 전략이 담당한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 얻을 수 있는 장점은 분명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;둘째, 코드가 문제의 구조를 더 잘 드러냅니다. 쿼터 시스템의 핵심은 &quot;어떤 기준으로 제한할 것인가&quot;인데, 이를 전략으로 분리해 두면 코드 수준에서도 정책이 독립된 개념으로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, 테스트가 쉬워집니다. 정책별 검증 규칙을 각각 분리해서 검증할 수 있기 때문에, 복잡한 분기문 하나를 통째로 테스트하는 것보다 훨씬 명확합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 전략 패턴이 만능은 아닙니다. 정책 조합이 아주 복잡해지면 resolver나 chain 같은 다른 구조가 필요할 수 있습니다. 하지만 적어도 이 단계에서는 &quot;정책 선택&quot;과 &quot;정책 실행&quot;을 분리하는 것만으로도 설계가 상당히 단순해졌습니다.&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;정책을 분리하고 나니 다음 문제가 남았습니다. &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;쿼터 시스템에서는 이 문제가 생각보다 빨리 드러납니다. 예를 들어 남은 횟수가 1회일 때 요청 두 개가 거의 동시에 들어오면, 둘 다 조회 시점에는 &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;처음에는 사용 이력을 한 테이블에 계속 insert 하면서 집계로 처리하는 방식을 떠올렸습니다. 그러나 이 방식은 현재 인프라 구조상 아래와 같은 한계를 가집니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL InnoDB의 격리 수준을 Repeatable Read로 설정했기 때문에 '조회하는 시점(스냅샷) '의 기능별 사용 횟수와 '현재' 기능별 사용 횟수가 다를 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서, 락을 사용할 수 있는 구조로 테이블을 재설계했습니다. 그리고, 사용 가능한 락을 검토했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;낙관적 락&lt;/li&gt;
&lt;li&gt;비관적 락&lt;/li&gt;
&lt;li&gt;분산 락&lt;/li&gt;
&lt;li&gt;조건부 원자적 update&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 낙관적 락&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락은 충돌이 드물다는 전제에서는 깔끔한 선택지입니다. 다만 충돌이 발생했을 때 재시도나 실패 처리가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 쿼터 차감이 사용자의 핵심 요청 흐름 안에 있다는 점이었습니다. 이 상황에서 &quot;잠시 후 다시 시도해 주세요&quot;를 자주 반환하는 구조는 사용자 경험 측면에서 부담이 컸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 기술적으로는 가능하지만 이 문제에 가장 잘 맞는 방식은 아니라고 판단했습니다.&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;2. 비관적 락&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;h4 data-ke-size=&quot;size20&quot;&gt;3. 분산 락&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락도 검토했습니다. 여러 인스턴스가 동시에 접근하는 환경에서 통제 지점을 외부로 빼는 방식은 분명 매력적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 방식은 새로운 인프라 의존성을 가져옵니다. 락 저장소, 만료 시간, 장애 시 복구 방식까지 함께 설계해야 합니다. 쿼터 차감 하나를 위해 시스템 복잡도를 그렇게까지 올릴 필요가 있는지는 의문이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 조건부 원자적 update&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 본 것이 조건부 update였습니다. 핵심은 검증과 갱신을 하나의 SQL 문 안에서 끝내는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE quota_counter
SET used_count = used_count + 1
WHERE id = ?
  AND used_count &amp;lt; quota_limit;&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;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;DB가 보장하는 원자성 안에서 문제를 해결할 수 있다.&lt;/li&gt;
&lt;li&gt;짧은 row-level lock만으로 처리된다.&lt;/li&gt;
&lt;li&gt;애플리케이션에서 별도 락 프로토콜을 만들지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 이 방식을 좋게 본 이유는, 쿼터 시스템의 핵심 문제를 가장 직접적으로 풀기 때문입니다. 이 문제에서 정말 필요한 것은 &quot;검증과 갱신을 절대 분리하지 않는 것&quot;이지, 더 화려한 락 전략이 아니었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종적으로 택한 방향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 저는 아래 두 가지를 중심으로 설계했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;정책은 전략으로 분리한다.&lt;/li&gt;
&lt;li&gt;카운터 증가는 조건부 원자적 update로 처리한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책의 복잡성과 동시성의 복잡성을 같은 레이어에서 한 번에 풀려고 하지 않고, 이 둘을 분리해서 생각을 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이 선택의 trade-off&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 장단점은 무엇일까 고민해봤습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 카운터 모델이 단순한 대신 유연성은 제한됩니다&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;h4 data-ke-size=&quot;size20&quot;&gt;2. 특정 카운터로 트래픽이 몰리면 병목이 생길 수 있습니다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건부 update는 단순하고 실용적이지만, 결국 같은 row에 요청이 집중되면 해당 지점이 병목이 될 수 있다고 생각합니다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험을 통해 얻은 것을 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 기술을 잘 선택하기 위해서는 기술에 대해서 잘 알고 있어야합니다.&lt;/p&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;셋째, 설계에서는 &quot;가장 유연한 구조&quot;보다 &quot;현재 문제를 가장 단순하게 푸는 구조&quot;가 더 좋은 선택이구나를 다시 한 번 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히, 요즘 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;&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;</description>
      <category>트러블 슈팅</category>
      <category>B2B</category>
      <category>락</category>
      <category>전략 패턴</category>
      <author>DS log</author>
      <guid isPermaLink="true">https://rasony.tistory.com/222</guid>
      <comments>https://rasony.tistory.com/222#entry222comment</comments>
      <pubDate>Thu, 2 Apr 2026 08:17:56 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 외래키 왜 사용하지 않는 것일까?</title>
      <link>https://rasony.tistory.com/220</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. &lt;br /&gt;&lt;br /&gt;이번 포스팅에서는 외래키 왜 사용하지 않는 것일까? 라는 주제로 공부하고 생각한 내용을 공유하려 합니다.&lt;br /&gt;&lt;br /&gt;먼저, 외래키가 무엇인지 알아야겠죠.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외래키는 무엇일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외래키는 테이블 간에 서로 연관된 데이터를 참조할 수 있게 하는 도구입니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;부모 테이블과 자식 테이블이 있다고 할 때, 부모 테이블은 참조되는 칼럼 값을 가지고 있는 테이블이고, 자식 테이블은 부모 테이블의 값을 참조하는 칼럼을 가진 테이블입니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;외래키 왜 존재하는 걸까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 외래키는 잘 사용하지 않습니다. 그럼에도 불구하고 MySQL에는 존재하고 있죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 MySQL에는 외래키라는 게 있는 지 생각해보았습니다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 개발을 하다보면, 의도한 데이터를 받으려고 Request에 Validation을 추가하곤 합니다.&lt;br /&gt;이것과 비슷한 목적으로 DB 상에 잘못된 데이터가 들어오는 것을 막기 위해서 외래키가 존재합니다.&lt;br /&gt;&lt;br /&gt;자식 테이블에 데이터를 넣을 때 부모 테이블에 존재하지 않는 ID를 넣는 것은 데이터 무결성을 해치는 일입니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;둘 째, &lt;b&gt;테이블간의 관계&lt;/b&gt;를 명시하기 위해 존재한다.&lt;br /&gt;&lt;br /&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;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;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 탈퇴 시 요구사항이 Hard Delete라면 외래키 설정만으로 연관 데이터를 쉽게 제거할 수 있습니다. (ON DELETE CASCADE)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;외래키를 사용하지 않는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;외래키가 설정된 경우, 부모 테이블과 자식 테이블간의 참조 무결성이 강제됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 긴급 조치가 필요한 경우 복잡한 관계로 인해 데이터를 수정하거나 삭제하는 게 어려워질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 째, 잠금 전파로 인한 &lt;b&gt;성능 저하&lt;/b&gt;가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외래키가 설정된 경우 부모 테이블과 자식 테이블간의 참조 무결성을 강제하기 위해서 수정, 삭제 연산이 발생하면 관련된 테이블에 확인 쿼리를 발생시킵니다.&lt;br /&gt;&lt;br /&gt;이로 인해서, 대기가 발생하며 심할 경우 데드락이 발생할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외래 키와 잠금이 어떤 관계가 있는 걸까?&lt;/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;br /&gt;&lt;br /&gt;InnoDB의 외래키는 아래와 같은 특징을 가지고 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블의 변경(쓰기 잠금)이 발생하는 경우에 잠금 대기가 발생합니다.&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;p data-ke-size=&quot;size16&quot;&gt;테이블의 변경이 발생하는 경우에 잠금 대기가 발생한다? 이게 무슨 말일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이해하기 위해 tb_parent와 tb_child를 사용해서 잠금 대기 상황을 예시로 들어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-09 오후 10.00.04.png&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEcKzW/btsMbCFSueI/YoJSad6NcJ17IckLGkpMBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEcKzW/btsMbCFSueI/YoJSad6NcJ17IckLGkpMBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEcKzW/btsMbCFSueI/YoJSad6NcJ17IckLGkpMBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEcKzW%2FbtsMbCFSueI%2FYoJSad6NcJ17IckLGkpMBK%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;600&quot; height=&quot;325&quot; data-filename=&quot;스크린샷 2025-02-09 오후 10.00.04.png&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;700&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;tb_child 테이블은 tb_parent 테이블에 대한 FK를 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 테이블에서 변경이 발생해 어떻게 대기가 발생하는 지 살펴보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;자식 테이블의 변경에 대기하는 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-09 오후 10.04.09.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Om5ZA/btsMbqlrn9j/ROxbo472yAfy16uUkHsC0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Om5ZA/btsMbqlrn9j/ROxbo472yAfy16uUkHsC0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Om5ZA/btsMbqlrn9j/ROxbo472yAfy16uUkHsC0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOm5ZA%2FbtsMbqlrn9j%2FROxbo472yAfy16uUkHsC0K%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;600&quot; height=&quot;241&quot; data-filename=&quot;스크린샷 2025-02-09 오후 10.04.09.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;부모 테이블의 id = 2 레코드에 수정이 발생&lt;br /&gt;&lt;br /&gt;커넥션 - 1이 부모 테이블 id = 2인 레코드에 배타 락 휙득&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;자식 테이블의 id = 100인 레코드가 FK인 pid를 2로 변경&lt;br /&gt;&lt;br /&gt;커넥션 - 2에서 부모 테이블 id = 2인 레코드가 존재하는 지 공유 락을 휙득 시도&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 테이블 id = 2인 레코드는 현재 잠금 대기 중인 상태이기 때문에 부모 테이블에 id = 2인 레코드가 존재하는 지 확인이 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;왜 확인이 불가능할까?&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;FK 검증 시 공유 락(S Lock)을 사용하여 해당 부모 레코드가 다른 트랜잭션에서 변경되지 않도록 보호하려고 합니다.&lt;br /&gt;하지만 현재 부모 테이블의 id = 2 레코드는 이미 배타 락(X Lock)이 걸려 있으므로, 공유 락(S Lock) 요청이 블로킹이 됩니다.&lt;br /&gt;결과적으로 FK 검증을 위한 확인이 불가능해집니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;부모 테이블의 변경 작업이 대기하는 경우&lt;br /&gt;&lt;br /&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-09 오후 10.18.44.png&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kAcjO/btsMdpyituY/NAzBaWh0zqkBduWx5FVE4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kAcjO/btsMdpyituY/NAzBaWh0zqkBduWx5FVE4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kAcjO/btsMdpyituY/NAzBaWh0zqkBduWx5FVE4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkAcjO%2FbtsMdpyituY%2FNAzBaWh0zqkBduWx5FVE4k%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;600&quot; height=&quot;264&quot; data-filename=&quot;스크린샷 2025-02-09 오후 10.18.44.png&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;자식 테이블의 id = 100인 레코드에 수정 발생&lt;br /&gt;&lt;br /&gt;커넥션 - 1에서 id = 100인 레코드에 배타 락 휙득&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;부모 테이블의 id = 1인 레코드를 삭제하려고 할 시에 외래키 속성(ON DELETE CASCADE) 때문에 id = 100인 레코드도 부모 레코드가 삭제될 시에 같이 삭제되어야 함.&lt;br /&gt;&lt;br /&gt;자식 테이블의 id = 100인 레코드에 배타 락이 걸려 있어 다른 트랜잭션에서 삭제가 불가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해보면, 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;첫 번째로 부모 테이블에서 변경 작업이 발생할 때, 자식 테이블에서 해당 변경된 레코드를 참조해야할 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 자식 테이블에서 특정 레코드에 대해 변경 작업이 이루어질 때, 동시에 부모 테이블에서 삭제 연산이 발생해 자식 테이블의 해당 레코드 또한 삭제 해야하는 경우 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지루한 글 읽어주셔서 감사합니다. 어려운 시기에 우리 모두 힘내길&lt;/p&gt;</description>
      <category>기술</category>
      <author>DS log</author>
      <guid isPermaLink="true">https://rasony.tistory.com/220</guid>
      <comments>https://rasony.tistory.com/220#entry220comment</comments>
      <pubDate>Sun, 9 Feb 2025 22:25:01 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 공유 락과 배타 락에 대해서 알아보자</title>
      <link>https://rasony.tistory.com/219</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;안녕하세요. 이번 포스팅에서는 InnoDB 스토리지 엔진의 S Lock과 X Lock에 대해서 알아보려고 합니다.&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;공유 락(Shared(S) Lock)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유 락은 락을 보유한 트랜잭션이 행을 읽을 수 있도록 허용하는 락입니다.&lt;br /&gt;&lt;br /&gt;예를 들어서, T1이 행 R에 S락을 보유 중일 때 T2의 S락 요청은 즉시 승인되지만 X 락 요청은 차단됩니다. 쉽게 말하면, 하나의 행에 대해서 공유 락만 휙득 가능하고 공유 락을 휙득한 트랜잭션만이 읽기 연산이 가능합니다.&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;p data-ke-size=&quot;size16&quot;&gt;아래와 같이&amp;nbsp;&lt;b&gt;SELECT FOR SHARE&lt;/b&gt;&amp;nbsp;를 사용하여 특정 데이터로부터 공유 락을 획득할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM table_name WHERE id = 1 FOR SHARE;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배타 락(Exclusive(X) Lock)&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;br /&gt;&lt;br /&gt;예를 들어서, T1이 행 R에 X락을 보유 중일 때 T2의 어떠한 락(X or S)도 즉시 승인되지 않으며, T1이 X락을 해제할 때까지 기다려야합니다. 즉, 하나의 행에 대해 배타 락을 휙득한 트랜잭션이 있다면 다른 트랜잭션은 해당 데이터에 대해 읽기 연산, 쓰기 연산 모두 수행할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이&amp;nbsp;&lt;b&gt;SELECT FOR UPDATE&lt;/b&gt; 를 사용하여 특정 데이터로부터 배타 락 을 획득할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;잠금 없는 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 데이터에 공유 락이나 배타 락이 걸려있더라도, 다른 트랜잭션에서 해당 데이터를 읽는 것이 불가능한 것이 아닙니다.&lt;br /&gt;이는 InnoDB의 MVCC(Multi Version Concurrency Control) 매커니즘 덕분입니다.&lt;br /&gt;&lt;br /&gt;일반적인 Select 쿼리는 락을 대기하지 않고 즉시 조회가 가능합니다.&lt;br /&gt;&lt;br /&gt;첫 번째 이유는 Undo Log를 활용하기 때문입니다.&lt;br /&gt;&lt;br /&gt;배타 락이 걸려 데이터가 수정 중이라도 InnoDB는 변경 전의 데이터를 Undo Log에 보관합니다.&lt;br /&gt;&lt;br /&gt;두 번째 이유는 Snapshot Read입니다.&lt;br /&gt;&lt;br /&gt;Repeatable Read 격리 수준에서 Select는 트랜잭션의 시작도니 시점의 Read Vies(스냅샷) 기준으로 데이터를 읽습니다.&lt;br /&gt;자신의 트랜잭션 ID보다 큰 트랜잭션이 변경한 데이터는 무시하고, Undo Log에서 자신의 ID보다 작거나 같은 데이터를 찾아 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 FOR SHARE나 FOR UPDATE 같은 &lt;b data-index-in-node=&quot;29&quot; data-path-to-node=&quot;8&quot;&gt;Locking Read&lt;/b&gt;가 아닌 단순 조회는 락의 영향을 받지 않고 자유롭게 수행될 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;지금까지 공유락과 배타락에 대해서 알아보았습니다. 감사합니다.  &lt;/p&gt;</description>
      <category>기술</category>
      <category>Snapshot Read</category>
      <category>공유락</category>
      <category>배타락</category>
      <category>잠금없는읽기</category>
      <author>DS log</author>
      <guid isPermaLink="true">https://rasony.tistory.com/219</guid>
      <comments>https://rasony.tistory.com/219#entry219comment</comments>
      <pubDate>Sun, 9 Feb 2025 21:14:59 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] READ COMMITED와 REPEATABLE READ 비교</title>
      <link>https://rasony.tistory.com/217</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;안녕하세요!&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이번 포스팅에서는 InnoDB의 격리 수준에 대해서 다뤄보려고 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 격리 수준(Isolation Level)은 ACID 원칙 중 하나로, 복수의 트랜잭션이 동시에 실행될 때 특정 트랜잭션이 다른 트랜잭션에서 수정한 데이터를 어느 정도 수준까지 가시적으로 허용할 것인지를 결정하는 핵심 설정입니다.&lt;br /&gt;&lt;br /&gt;InnoDB는 기본적으로 &lt;b data-index-in-node=&quot;157&quot; data-path-to-node=&quot;5&quot;&gt;REPEATABLE READ&lt;/b&gt;를 채택하여 높은 수준의 일관성을 제공하지만, 성능 최적화나 데드락 감소가 필요한 경우 &lt;b data-index-in-node=&quot;221&quot; data-path-to-node=&quot;5&quot;&gt;READ COMMITTED&lt;/b&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;h3 data-ke-size=&quot;size23&quot;&gt;READ COMMITTED&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;READ COMMITTED 격리 수준에서 가장 중요한 개념은 동일 트랜잭션 내에서도 각 일관된 읽기마다 최신 스냅샷을 설정하고 읽는 다는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 일관된 읽기마다 스냅샷을 보기 때문에 커밋된 데이터만 읽을 수 있습니다. 이 덕분에 Dirty Read 문제는 발생하지 않고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그렇지만, 동일 트랜잭션 내에서 각 일관된 읽기마다 최신 스냅샷을 보기 때문에 &amp;ldquo;하나의 트랜잭션에 같은 조회 쿼리를 여러번 수행하더라도 결과가 같아야 한다&amp;rdquo;는 NON-REPEATABLE READ 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 예시로 확인해보겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-06 오후 4.55.54.png&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yXGPu/btsLGiGkQ1M/k1klWkQAsM5YzDYvcgYOzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yXGPu/btsLGiGkQ1M/k1klWkQAsM5YzDYvcgYOzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yXGPu/btsLGiGkQ1M/k1klWkQAsM5YzDYvcgYOzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyXGPu%2FbtsLGiGkQ1M%2Fk1klWkQAsM5YzDYvcgYOzK%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;600&quot; height=&quot;498&quot; data-filename=&quot;스크린샷 2025-01-06 오후 4.55.54.png&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;854&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;사용자 B가 한 TX에서 first_name이 Toto인 직원을 처음 조회을 때는 결과가 없었습니다. 이후 다른 TX에서 기존에 Lara라는 first_name을 가진 직원의 first_name을 'Toto'로 변경했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 B는 아까와 같은 TX에서 다시 한 번 first_name이 'Toto'인 직원을 조회했는데 이번에는 해당 직원에 대해서 조회가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼, READ COMMITED는 동일 TX내에서 각 일관된 읽기마다 최신 스냅샷을 설정하고 읽는 특징 때문에 같은 TX 내에서 같은 조회 쿼리를 수행했을 때 결과가 달리질 수도 있습니다.&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;REPEATABLE READ&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;REPEATABLE READ는 READ COMMITED와 다르게 동일 트랜잭션 내의 일관된 읽기는 첫 번째 읽기 시점에 생성된 스냅샷을 읽습니다.&lt;br /&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;그 원리는 TRX_ID에 있습니다.&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;TRX_ID는 트랜잭션의 고유한 번호로 계속 증가합니다. 하나의 행에는 이 행이 생성 혹은 수정될 때 이에 영향을 준 TRX_ID가 저장이 되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REPEATABLE READ에서는 자신의 TRX_ID보다 낮은 TRX_ID를 가진 행들만 접근할 수 있게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하여서 NON-REPEATABLE READ 문제를 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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-filename=&quot;스크린샷 2025-01-06 오후 4.56.44.png&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AYSYS/btsLFRvH1NE/pdjvHcKJ9MMspcl4M4RusK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AYSYS/btsLFRvH1NE/pdjvHcKJ9MMspcl4M4RusK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AYSYS/btsLFRvH1NE/pdjvHcKJ9MMspcl4M4RusK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAYSYS%2FbtsLFRvH1NE%2FpdjvHcKJ9MMspcl4M4RusK%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;600&quot; height=&quot;604&quot; data-filename=&quot;스크린샷 2025-01-06 오후 4.56.44.png&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;844&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;10번 트랜잭션은 TRX_ID가 같거나 낮은 트랜잭션에서 변경된 레코드에만 접근할 수 있기 때문에 12번 트랜잭션에서 id = 500000인 레코드를 수정한 결과가 반영된 레코드에는 접근할 수 없습니다.&lt;/p&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;정리하자면, READ COMMITTED와 REPEATABLE READ 둘 다 commit된 데이터를 보여주는 것은 동일하지만, REPEATABLE READ에서는 조회하는 트랜잭션 ID보다 낮은 트랜잭션 ID를 가진 행에 대해서만 접근할 수 있는게 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다!&lt;/p&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;br /&gt;MVCC와 Undo Log에 대해서 궁금하시다면, 아래 포스팅을 읽어주시면 감사하겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://rasony.tistory.com/215&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://rasony.tistory.com/215&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773149393365&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[MySQL] MVCC에 대해서 알아보자&quot; data-og-description=&quot;안녕하세요! 오늘은 InnoDB가 Multi Version을 관리하는 방식에 대해서 다루려고 합니다. Multi Version을 다루는 것을 MVCC라고 부르는데요. 이는 Multi Version Concurrency Control의 약자입니다.직역하자면, 다중&quot; data-og-host=&quot;rasony.tistory.com&quot; data-og-source-url=&quot;https://rasony.tistory.com/215&quot; data-og-url=&quot;https://rasony.tistory.com/215&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/biDQWS/dJMb89ybPgw/xaEzAAXU70Xq1b7oVbpUz1/img.png?width=742&amp;amp;height=518&amp;amp;face=0_0_742_518,https://scrap.kakaocdn.net/dn/dtMW26/dJMb87NUECH/saPeWsWux2wkTR6icuCXg1/img.png?width=742&amp;amp;height=518&amp;amp;face=0_0_742_518,https://scrap.kakaocdn.net/dn/btbqcs/dJMb86OZ36c/Mq0Fw6pVJVZk78vZsEjfA1/img.png?width=816&amp;amp;height=690&amp;amp;face=100_44_464_400&quot;&gt;&lt;a href=&quot;https://rasony.tistory.com/215&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://rasony.tistory.com/215&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/biDQWS/dJMb89ybPgw/xaEzAAXU70Xq1b7oVbpUz1/img.png?width=742&amp;amp;height=518&amp;amp;face=0_0_742_518,https://scrap.kakaocdn.net/dn/dtMW26/dJMb87NUECH/saPeWsWux2wkTR6icuCXg1/img.png?width=742&amp;amp;height=518&amp;amp;face=0_0_742_518,https://scrap.kakaocdn.net/dn/btbqcs/dJMb86OZ36c/Mq0Fw6pVJVZk78vZsEjfA1/img.png?width=816&amp;amp;height=690&amp;amp;face=100_44_464_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[MySQL] MVCC에 대해서 알아보자&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 InnoDB가 Multi Version을 관리하는 방식에 대해서 다루려고 합니다. Multi Version을 다루는 것을 MVCC라고 부르는데요. 이는 Multi Version Concurrency Control의 약자입니다.직역하자면, 다중&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;rasony.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기술</category>
      <category>InnoDB 격리수준</category>
      <category>read committed</category>
      <category>Repeatable read</category>
      <author>DS log</author>
      <guid isPermaLink="true">https://rasony.tistory.com/217</guid>
      <comments>https://rasony.tistory.com/217#entry217comment</comments>
      <pubDate>Mon, 6 Jan 2025 16:57:41 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] InnoDB 스토리지 엔진 수준의 락</title>
      <link>https://rasony.tistory.com/216</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이번 포스팅에서는 InnoDB 스토리지 엔진의 락에 관해서 다뤄보려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;MySQL의 Default 스토리지 엔진, InnoDB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;MySQL의 기본 엔진은 InnoDB 스토리지 엔진입니다.&lt;br /&gt;&lt;br /&gt;여러가지 이유가 있겠지만 그 이유 중 하나는 InnoDB 스토리지 엔진은 레코드 기반의 잠금을 제공하기 때문에 MyISAM 엔진보다 뛰어난 동시성 처리를 제공합니다. &lt;br /&gt;&lt;br /&gt;또한, 잠금 정보가 상당히 작은 공간에서 관리되기 때문에 레코드 락이 페이지 락 또는 테이블 락으로 레벨업되지 않는 특징도 이유 중 하나입니다.&lt;br /&gt;&lt;br /&gt;사실 가장 큰 이유는 MyISAM 엔진은 트랜잭션을 지원하지 않습니다.. ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 InnoDB 스토리지 엔진에서 지원하는 Lock에 대해서 알아보겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;락(Record Lock)&lt;br /&gt;&lt;br /&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 레코드 락(record lock)은 테이블의 특정 레코드를 잠그는 기능으로 이해됩니다. 그러나, InnoDB 엔진의 레코드 락은 테이블의 레코드를 직접 잠그는 것이 아니라, &lt;b&gt;인덱스 레코드&lt;/b&gt;를 잠급니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개념을 이해하기 위해 &lt;b&gt;특정 레코드를 수정&lt;/b&gt;하는 상황을 살펴보겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;인덱스를 사용하는 경우&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;특정 레코드를 수정하기 위해 조회시 인덱스를 활용했다면, MySQL은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;해당 레코드뿐만 아니라 조회에 사용된 인덱스 레코드를 잠금을 수행합니다. 이는 동일한 인덱스를 통해 접근할 수 있는 다른 레코드들에도 잠금이 적용된다는 뜻입니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테이블을 풀 스캔하는 경우&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;테이블에 명시적인 인덱스가 하나도 없더라도 항상 인덱스 레코드에 락을 겁니다. 인덱스가 정의되지 않은 경우 InnoDB는 내부적으로 숨겨진 클러스터형 인덱스를 생성하고 이를 사용하여 레코드 락을 수행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, MySQL의 레코드 락은 조회 방식에 따라 잠금의 범위가 달라집니다. 따라서, 효율적인 잠금 관리를 위해 인덱스 설계가 매우 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;ldquo;인덱스 레코드를 잠근다&amp;rdquo; 이게 뭘까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;인덱스 레코드에 락이 걸린다는 것은 다순히 행에 락이 걸리는 것이 아닙니다. B+ 트리의 특정 노드에 저장된 인덱스 엔트리에 잠금이 걸리는 것입니다.&lt;br /&gt;&lt;br /&gt;클러스터형 인덱스와 세컨더리 인덱스 모두 각각 구조는 살짝 다르지만 B+ 트리 구조입니다.&amp;nbsp; 둘 모두 중간 노드를 거쳐서 리프 노드에 도달하게 됩니다. 이 리프 노드에 락이 걸리게 됩니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;한 가지 예시를 들어보겠습니다.&lt;br /&gt;&lt;br /&gt;조회 시 세컨더리 인덱스를 탐색해 세컨더리 인덱스 트리의 중간 노드를 거쳐 리프 노드에 도달합니다. 이후 해당 레코드에 락이 걸립니다.&lt;br /&gt;&lt;br /&gt;다음으로, 세컨더리 인덱스 리프 노드에 저장된 PK 값을 가지고 클러스터형 인덱스 트리로 이동 후 클러스터형 인덱스의 리프 노드에 도달하게 됩니다. 또, 실제 데이터가 포함된 레코드에도 락이 걸립니다.&lt;br /&gt;&lt;br /&gt;이번에는 실제 쿼리 예시를 들어보겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;employees 테이블에 first_name 컬럼과 last_name컬럼이 존재하고, first_name 컬럼에만 인덱스가 걸려있다고 가정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-06 오후 4.47.06.png&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cw3K4/btsLFYg00Xe/N65gNOKGIke587RqEBkxnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cw3K4/btsLFYg00Xe/N65gNOKGIke587RqEBkxnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cw3K4/btsLFYg00Xe/N65gNOKGIke587RqEBkxnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCw3K4%2FbtsLFYg00Xe%2FN65gNOKGIke587RqEBkxnk%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;600&quot; height=&quot;193&quot; data-filename=&quot;스크린샷 2025-01-06 오후 4.47.06.png&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 조회 결과&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;first_name이 'Georgi'인 레코드는 &lt;b&gt;253개&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;first_name이 'Georgi'이고 last_name이 'Klassen'인 레코드는 &lt;b&gt;1개&lt;/b&gt;입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-06 오후 4.47.23.png&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmhmD0/btsLEqlJD4C/kjZYfAqsC0p9WaMZbD08m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmhmD0/btsLEqlJD4C/kjZYfAqsC0p9WaMZbD08m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmhmD0/btsLEqlJD4C/kjZYfAqsC0p9WaMZbD08m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmhmD0%2FbtsLEqlJD4C%2FkjZYfAqsC0p9WaMZbD08m0%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;600&quot; height=&quot;53&quot; data-filename=&quot;스크린샷 2025-01-06 오후 4.47.23.png&quot; data-origin-width=&quot;1114&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 절에서 first_name 컬럼만 인덱스를 사용하므로, MySQL은 인덱스 레인지 스캔(index range scan)을 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;first_name에 설정된 인덱스를 통해 접근 가능한 레코드는 253개입니다.&lt;/li&gt;
&lt;li&gt;따라서, &lt;b&gt;253개의 레코드에 레코드 락&lt;/b&gt;이 걸립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-06 오후 4.47.44.png&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEx6dl/btsLFOeGdQR/a2Lfu56YK3ZhpdYCEtqTSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEx6dl/btsLFOeGdQR/a2Lfu56YK3ZhpdYCEtqTSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEx6dl/btsLFOeGdQR/a2Lfu56YK3ZhpdYCEtqTSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEx6dl%2FbtsLFOeGdQR%2Fa2Lfu56YK3ZhpdYCEtqTSK%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;600&quot; height=&quot;365&quot; data-filename=&quot;스크린샷 2025-01-06 오후 4.47.44.png&quot; data-origin-width=&quot;956&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;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;갭 락(Gap Lock)&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;갭 락(Gap Lock)은 &lt;b&gt;인덱스 레코드 사이의 간격&lt;/b&gt;에 대한 잠금입니다. 또는 첫 번째 인덱스 레코드 이전이나 마지막 인덱스 레코드 이후의 간격에 대한 잠금을 의미합니다.&lt;br /&gt;&lt;br /&gt;Gap Lock은 Repeatable Read 이상의 격리 수준에서 사용되며, 그 이하의 격리 수준에서는 비활성화할 수 있습니다.&lt;br /&gt;&lt;br /&gt;이 Gap Lock의 유일한 목적은 다른 트랜잭션이 이 간격에 데이터를 삽입하는 것을 방지하는 것입니다. 그렇기 때문에 Gap Lock은 공존할 수 있습니다.&lt;br /&gt;&lt;br /&gt;그리고, 고유 인덱스를 사용하여 고유한 행을 검색하는 구문에는 Gap Lock이 필요하지 않습니다.&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;넥스트 키 락(Next Key Lock)&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;span data-token-index=&quot;0&quot;&gt;Next-Key Lock&lt;/span&gt;은 인덱스 레코드에 대한 레코드 락과 해당 인덱스 레코드 앞의 갭에 대한 갭 락이 결합된 형태입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next-Key Lock에 대해서 예를 들어보자면,,, 특정 레코드 R에 대해 레코드 락이 걸려있고, 그 앞의 간격에 Gap Lock이 걸려져 있는 상태입니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이 경우 다른 세션에서 R 앞의 간격에 새로운 인덱스 레코드를 삽입할 수 없습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;왜 앞의 간격에 Gap Lock이 걸리는 거지?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-path-to-node=&quot;5,2&quot;&gt;&lt;span&gt;InnoDB에서 &lt;/span&gt;&lt;b data-index-in-node=&quot;10&quot; data-path-to-node=&quot;5,2&quot;&gt;&lt;span&gt;Next-Key Lock&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 인덱스 레코드의 **이전 간격(Preceding Gap)**에 걸리는 이유는 B+ 트리의 물리적 구조와 오름차순 스캔 방식 때문입니다&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;5,3&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;5,4&quot;&gt;.&lt;/span&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-path-to-node=&quot;5,6&quot;&gt;&lt;span&gt;InnoDB는 인덱스를 검색하거나 스캔할 때 만나는 레코드마다 잠금을 설정하는데, 이때 &lt;/span&gt;&lt;b data-index-in-node=&quot;49&quot; data-path-to-node=&quot;5,6&quot;&gt;&lt;span&gt;'해당 레코드'와 '그 직전 레코드 사이의 간격'을 하나의 단위&lt;/span&gt;&lt;/b&gt;&lt;span&gt;로 묶어 잠그게 됩니다&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;5,7&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;5,8&quot;&gt;. &lt;/span&gt;&lt;span data-path-to-node=&quot;5,9&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;5,10&quot;&gt;&lt;span&gt;결과적으로 스캔 범위 내의 모든 레코드에 대해 이 메커니즘이 적용되면서, 기존 레코드 사이에 새로운 데이터가 끼어드는 Phantom Row 생성을 원천적으로 차단하여 Repeatable Read 격리 수준을 보장할 수 있게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;이상으로 InnoDB 스토리지 엔진의 락에 대한 포스팅을 마무리 하겠습니다. 힘든 시기에 다들 파이팅입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;출처&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RealMySQL 8.0&lt;br /&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html#innodb-shared-exclusive-locks&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html#innodb-shared-exclusive-locks&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기술</category>
      <category>갭 락</category>
      <category>레코드 락</category>
      <author>DS log</author>
      <guid isPermaLink="true">https://rasony.tistory.com/216</guid>
      <comments>https://rasony.tistory.com/216#entry216comment</comments>
      <pubDate>Mon, 6 Jan 2025 16:53:46 +0900</pubDate>
    </item>
  </channel>
</rss>