<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발자, 퇴근 후 실험 중</title>
    <link>https://nu-stock.tistory.com/</link>
    <description>AI가 많은 이 시대에서 FOMO가 온 개발자는 무엇으로 살아남을 수 있을지 테스트중입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 9 Oct 2026 23:30:28 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>누스타악</managingEditor>
    <image>
      <title>개발자, 퇴근 후 실험 중</title>
      <url>https://tistory1.daumcdn.net/tistory/7089563/attach/824733e683394c09b733c7b8c70c2230</url>
      <link>https://nu-stock.tistory.com</link>
    </image>
    <item>
      <title>[Kafka] Topic, Partition, Consumer Group은 어떻게 연결될까?</title>
      <link>https://nu-stock.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 토스 면접에서 kafka에 대해서 질문을 줬지만 난 대답하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜? 잘 모르니까..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 얼레벌레 써봤지.. 뭘 제대로 알았겠어..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 후회돼서 복기해본다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kafka에 대한 큰 흐름&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;토픽(topic)은 물리적인 메시지 스트림이고, 물리적으로는 여러 개의 파티션(partition)으로 쪼개진다고 한다.&lt;/li&gt;
&lt;li&gt;파티션은 추가만 되는 순서 있는 로그라고 보면되고 메시지마다 오프셋(offset)이라는 단조 증가 번호가 붙는다.&lt;/li&gt;
&lt;li&gt;순서 보장은 &quot;파티션 안에서만&quot; 되고, 토픽 전체로는 순서를 보장하지 않는다.&lt;/li&gt;
&lt;li&gt;&quot;같은 키는 같은 파티션으로 보낸다&quot;가 중요한 포인트&lt;/li&gt;
&lt;li&gt;프로듀서는 메시지 키가 있으면 &lt;b&gt;hash(key) % 파티션 수&lt;/b&gt;로 파티션을 고르고, 키가 없으면 스티키 파티셔너로 분리한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 순서가 중요한 단위 (예: 같은 주문 ID, 같은 판매자 ID)는 키로 묶어야 그 단위 안에서 순서가 지켜진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;복제 (Replication) : 각 파티션은 리더 1개 + 팔로워 N개로 복제된다. 읽기/쓰기는 리더가 처리하고 팔로워가 따라 온다. 따라잡고 있는 복제본 집합을 ISR(In-Sync Replicas)라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토픽이란?&lt;/b&gt;&lt;/p&gt;
&lt;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;메시지를 보내는 쪽(프로듀서)는 &quot;이 메시지를 어느 토픽에 적을까?&quot;를 정하구, 받는 쪽(컨슈머)는 &quot;나는 어느 토픽을 읽을까?&quot;를 정한다.&lt;/li&gt;
&lt;li&gt;주문 토픽을 구독한 컨슈머는 주문 메시지만 받지, 결제 메시지는 받지 않는다.&lt;/li&gt;
&lt;li&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;&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;토픽이 이름이었다면? 파티션은 실제로 메시지가 줄줄이 쌓이는 진짜 통!&lt;/li&gt;
&lt;li&gt;orders 토픽을 파티션 3개로 만들면 주문이 들어올 때마다 셋 중 하나의 파티션에 가서 맨 뒤에 한 줄씩 추가된다.&lt;/li&gt;
&lt;li&gt;글을 적을 때 항상 맨 아래 빈 줄에 이어 쓰는 것과 같다. 중간에 끼워넣거나 지우지 않고, 무조건 뒤에만 붙인다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가만 되는 로그&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브로커(broker)란?&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;당시 운영 카프카는 브로커 3대 클러스터&lt;/li&gt;
&lt;li&gt;replication factor(복제 계수) : 복사본을 총 몇 개로 유지할지 정하는 숫자
&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;replication factor = 1 &amp;rarr; 원본 하나뿐, 복사본 없음 (그 브로커 죽으면 데이터 날아감)&lt;/li&gt;
&lt;li&gt;replication factor = 2 &amp;rarr; 원본 1 + 복사본 1 = 총 2벌&lt;/li&gt;
&lt;li&gt;replication factor = 3 &amp;rarr; 원본 1 + 복사본 2 = 총 3벌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In-Sync Replicas (ISR) - &quot;잘 따라오고 있는 복사본&quot;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복사본(팔로워)이 3벌 있다고 해서, 그게 항상 원본이랑 똑같다는 보장은 없다.&lt;/li&gt;
&lt;li&gt;네트워크가 느리거나 한 브로커가 버벅대면, 어떤 팔로워는 리더가 받은 최신 메시지를 아직 못 베낀 채 &lt;b&gt;뒤처질 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;In-Sync (싱크 맞음) : 리더를 거의 실시간으로 따라잡고 있는 복사본&lt;/li&gt;
&lt;li&gt;Out-of-Sync (뒤처짐) : 너무 늦어서 신뢰할 수 없는 복사본&lt;/li&gt;
&lt;li&gt;리더가 죽었을 때, 새 리더는 반드시 ISR 안에 있는 복사본 중에서만 뽑기 때문에 ISR은 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;병렬 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통이 3개니까, 컨슈머 3명이 각자 하나씩 동시에 읽을 수 있다.&lt;/li&gt;
&lt;li&gt;처리 속도를 올리고 싶으면 파티션을 늘린다&lt;/li&gt;
&lt;li&gt;컨슈머를 몇 개 띄울지는 개발자가 정해서 직접 실행한다.&lt;/li&gt;
&lt;li&gt;보통 애플리케이션(서버) 인스턴스를 여러 개 띄우거나, 한 애플리케이션 안에서 컨슈머 스레드 수를 늘리는 식&lt;/li&gt;
&lt;/ul&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;b&gt;컨슈머 그룹과 파티션의 관계&amp;nbsp;&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;반대로, 하나의 컨슈머는 여러 파티션을 동시에 소비할 수 있다.&lt;/li&gt;
&lt;li&gt;결론
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 그룹의 최대 병렬 처리량은 파티션 개수로 제한된다.&lt;/li&gt;
&lt;li&gt;컨슈머를 파티션보다 많이 띄워도, 초과분은 그냥 논다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNNfz9/dJMcahLDYuX/AXTGk4YY0rf9xKy9xlwLJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNNfz9/dJMcahLDYuX/AXTGk4YY0rf9xKy9xlwLJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNNfz9/dJMcahLDYuX/AXTGk4YY0rf9xKy9xlwLJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNNfz9%2FdJMcahLDYuX%2FAXTGk4YY0rf9xKy9xlwLJ1%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;547&quot; height=&quot;226&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티션이 4개면 아무리 컨슈머를 늘려도 동시에 4개까지만 일한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처리량을 더 늘리려면 컨슈머가 아니라 파티션을 늘려야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;반대로 컨슈머가 파티션보다 적으면(컨슈머 2개) 한 컨슈머가 여러 파티션을 나눠 가진다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;죽지는 않지만 그만큼 부하가 몰린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다른 컨슈머 그룹은 같은 파티션을 독립적으로 또 읽는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그룹마다 오프셋을 따로 관리하기 때문이다.&lt;/li&gt;
&lt;li&gt;토픽에 그룹/토픽/파티션 단위로 저장&lt;/li&gt;
&lt;li&gt;&quot;같은 메시지를 여러 시스템이 각자 소비&quot;가 가능한 이유.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;poll 루프와 리밸런싱&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;컨슈머는 어떻게 &quot;살아있다&quot;고 인정받는가? (heartbeat.interval.ms vs session.timeout.ms vs max.poll.interval.ms)&lt;/li&gt;
&lt;li&gt;poll()이 한 번에 가져온 배치를 처리하는 데 너무 오래 걸리면 무슨 일이 벌어지는가?&lt;/li&gt;
&lt;li&gt;그게 왜 &lt;b&gt;리밸런싱 폭풍 &amp;rarr; 컨슈머 증식 &amp;rarr; 중복 처리&lt;/b&gt;로 번지는가?&lt;/li&gt;
&lt;li&gt;AckMode.BATCH였던 게 왜 불에 기름을 부었는가?&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>kafka</category>
      <category>면접</category>
      <category>백엔드</category>
      <author>누스타악</author>
      <guid isPermaLink="true">https://nu-stock.tistory.com/33</guid>
      <comments>https://nu-stock.tistory.com/33#entry33comment</comments>
      <pubDate>Wed, 8 Jul 2026 09:03:25 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처] 우리 서비스는 왜 모놀리식과 MSA를 함께 사용하고 있을까?</title>
      <link>https://nu-stock.tistory.com/32</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;내가 운영하는 구조는 완전한 MSA라기보다는, 모놀리식과 일부 분리된 서비스가 공존하는 하이브리드 구조에 가깝다.&lt;br /&gt;display-api, order-api, ssr 등은 서비스 단위로 나뉘어 있지만, 데이터베이스나 레거시 도메인 의존성이 완전히 분리되어 있지는 않기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해보기 전에 우선 MSA가 뭔지 좀 생각해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MSA의 핵심은?&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서비스 독립성.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 서비스가 DB서버가 분리되어져있고, 독립적으로 배포됨&lt;/li&gt;
&lt;li&gt;모놀리식은 하나의 코드베이스에 모든 기능이 묶여 있어서 장바구니 버그 하나가 주문 전체를 죽이는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서비스 간 통신
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;직접 함수 호출 대신 Rest API(동기) 또는 Kafka 같은 메시지 브로거(비동기)로 통신&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장애 격리
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Circuit Breaker가 특정 서비스 장애가 전체로 번지지 않도록 차단.&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;1380&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B3m7B/dJMcabqUwnN/UiyZxKYaJ7lYnAYPS956N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B3m7B/dJMcabqUwnN/UiyZxKYaJ7lYnAYPS956N1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B3m7B/dJMcabqUwnN/UiyZxKYaJ7lYnAYPS956N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB3m7B%2FdJMcabqUwnN%2FUiyZxKYaJ7lYnAYPS956N1%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;727&quot; height=&quot;426&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;808&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;DB 분리 여부?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;MSA에서 제일 중요한 포인트 중 하나인 서비스별 DB 소유권이 없다&lt;/li&gt;
&lt;li&gt;MSA라고 부르기 위해서는 단순히 API 서버가 여러 개로 나뉜 것만으로는 부족하고 각 서비스가 자신의 데이터를 직접 소유하고,다른 서비스의 DB를 참조하지 않아야 하지만 현재는 그런 상태가 아니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분산 모놀리식?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;내가 처한 상태와 비슷한 걸 서버는 여러 개로 나뉘었지만, 배포&amp;middot;DB&amp;middot;도메인 규칙&amp;middot;장애 영향도가 여전히 강하게 묶여 있다면 이것은 MSA라기보다 분산 모놀리식에 가깝다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;하지만 MSA를 한다고 무조건 좋을까?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;네트워크 호출 실패 가능성
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모놀리식은 함수 호출이지만 MSA는 네트워크 호출이다.&lt;/li&gt;
&lt;li&gt;따라서, 나는 FeignClient로 추천 API를 호출할 때 Circuit Breaker를 걸었고, fallback을 빈 리스트로 처리하여 장애 및 실패 가능성에 대비한 구조로 개발했다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;API 응답 지연
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;네트워크 레이턴시가 추가된다.&lt;/li&gt;
&lt;li&gt;모놀리식은 서버 안에서 함수를 호출하기 때문에 레이턴시가 거의 0입니다. 하지만 MSA는 서비스 간 호출할 때마다 네트워크를 타고, 호출이 3번이면 레이턴시도 3번 누적된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFrq9U/dJMcaaS4oVx/hhkLGHf5QKYLzMfIfL7Thk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFrq9U/dJMcaaS4oVx/hhkLGHf5QKYLzMfIfL7Thk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFrq9U/dJMcaaS4oVx/hhkLGHf5QKYLzMfIfL7Thk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFrq9U%2FdJMcaaS4oVx%2FhhkLGHf5QKYLzMfIfL7Thk%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;715&quot; height=&quot;448&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터 정합성 관리 어려움
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모놀리식은 주문 생성, 장바구니 비우기, 재고 차감을 하나의 트랜잭션으로 묶을 수 있는 구조이다.&lt;/li&gt;
&lt;li&gt;MSA는 DB가 분리되면 묶고 싶어도 묶을 수 없는 구조입니다. DB가 서비스마다 다르니까 Spring @Transactional 이 아예 작동하지 않는다. 트랜잭션 자체가 DB 단위로 묶이기 때문입니다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;해결방법 : saga패턴, outbox패턴&lt;/li&gt;
&lt;li&gt;Saga 패턴은 각 서비스가 실패 시 보상 트랜잭션을 실행하는 방식&lt;/li&gt;
&lt;li&gt;아웃박스 패턴은 이벤트를 DB에 먼저 저장하고 Kafka로 발행해 원자성을 보장하는 방식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;면절 질문&lt;/b&gt; : DB 분리하면 어떻게 정합성 보장할 건가요?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;로그 추적 어려움
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모놀리식 로그는 한 곳에 있지만, MSA는 서비스마다 로그가 흩어진다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;배포 단위는 나뉘었지만 장애 원인 파악은 더 어려워짐
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;이건 앞의 로그 추적 어려움과 연결된다. 독립 배포가 가능해진 대신, 어느 서비스의 어느 배포가 문제를 일으켰는지 파악하는 복잡도가 올라간다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;운영 복잡도 증가&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;서비스가 늘어날수록 모니터링, 배포 파이프라인, 인프라 관리 비용이 급격히 올라간다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;완전한 MSA가 필요한 시점은?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;서비스 규모가 커져서 팀이 서로 방해가 될 때, DB를 공유하면 한 팀이 스키마를 바꿨을 때 다른 팀 서비스가 터진다.&lt;/li&gt;
&lt;li&gt;서비스별로 트래픽 차이가 극단적일 때, 쿠팡이츠로 예를 들면 주문 서비스는 점심/저녁 피크에 폭발적으로 트래픽이 몰리지만, 정산 서비스는 하루 한 번 돌아도 된다. DB까지 분리해야 주문 서비스만 독립적으로 스케일 아웃이 가능하다.&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;b&gt;우리 조직?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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까지 분리하기에는 비합리적이다. 팀이 1개밖에 없고, 파트도 업무도 분리되어져 있지 않고, MAU도 너무 낮다. 너무 복잡도가 높아질 확률이 높다. 그래서 우리는 DB를 공유하는 논리적 MSA단계였던 것 같다. 팀 규모와 서비스 성숙도를 고려했을 때 당시에는 합리적인 선택이었고, DB 분리는 서비스가 더 성장하고 팀 간 의존성이 문제가 되는 시점에 Saga 패턴과 함께 가져가야 할 다음 과제로 인식하고 있다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>  개발 성장 로그</category>
      <category>MSA</category>
      <category>개발자</category>
      <category>면접대비</category>
      <category>모놀리식</category>
      <category>백엔드</category>
      <author>누스타악</author>
      <guid isPermaLink="true">https://nu-stock.tistory.com/32</guid>
      <comments>https://nu-stock.tistory.com/32#entry32comment</comments>
      <pubDate>Thu, 4 Jun 2026 10:55:09 +0900</pubDate>
    </item>
    <item>
      <title>[면접 회고] 배치를 운영했지만 배치를 이해하진 못했다</title>
      <link>https://nu-stock.tistory.com/31</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;배치 관련 경험은 꽤 있다고 생각했다. 장애가 발생하면 로그를 보고, 매일 배치 로그가 정상인지 확인했으니까..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 면접에서 배치 서버가 어떻게 되어있냐고 물어보자 생각보다 답하지 못했던 질문들이 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인프라 토폴로지 = 시스템들이 어떻게 연결되어 있는지 그 전체 지도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[CLOUD&amp;nbsp;-&amp;nbsp;AWS]&lt;/b&gt;&lt;br /&gt;- display_api&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr; 상품상세 등 전시 API&lt;br /&gt;-&amp;nbsp;order_api&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;주문서,&amp;nbsp;장바구니&amp;nbsp;API&amp;nbsp;&amp;nbsp;&lt;br /&gt;- ssr&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr; Vue.js 신규 화면&lt;br /&gt;&lt;br /&gt;&lt;b&gt;[IDC - 온프레미스]&lt;/b&gt;&lt;br /&gt;-&amp;nbsp;batch&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;배치&amp;nbsp;서버&amp;nbsp;(단독)&lt;br /&gt;-&amp;nbsp;bo&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;백오피스&lt;br /&gt;- main&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; 메인 서비스&lt;br /&gt;- work&amp;nbsp; &amp;nbsp;&amp;rarr; kafka 서버 (배치와 같은 서버)&lt;br /&gt;-&amp;nbsp;redis-cache&amp;nbsp;&amp;rarr;&amp;nbsp;캐시&lt;br /&gt;- redis-session &amp;rarr; 세션&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;1336&quot; data-origin-height=&quot;1088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coDkJI/dJMcaaS3eMZ/JSjEaQeMOuBcBhKJMZBItK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coDkJI/dJMcaaS3eMZ/JSjEaQeMOuBcBhKJMZBItK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coDkJI/dJMcaaS3eMZ/JSjEaQeMOuBcBhKJMZBItK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoDkJI%2FdJMcaaS3eMZ%2FJSjEaQeMOuBcBhKJMZBItK%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;660&quot; height=&quot;537&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;1088&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;1. 배치 서버는 어디서 돌아가고 있는가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 나는 배치를 운영하고 있었지만 인프라 토폴로지를 제대로 설명해본 적이 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 시스템은 AWS Cloud와 IDC(On-premise)가 공존하는 과도기 구조.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Cloud 영역에는 전시 API(display_api), 주문 API(order_api), Vue 기반 SSR 서버가 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반면 IDC에는 batch, bo, main, Kafka(work), Redis(Cache/Session)가 운영되고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;배치서버
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;온프레미스&lt;/li&gt;
&lt;li&gt;단독 서버로 애플리케이션 서버와 공유하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실행방식
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;배치 트리거는 Spring의 ThreadPoolTaskScheduler를 직접 구현한 커스텀 스케쥴러를 사용&lt;/li&gt;
&lt;li&gt;따라서, Cron Trigger, Fixed Delay, Fixed Rate 3가지 방식을 배포 없이 BO에서 스케쥴 방식과 실행 시간 변경 가능&lt;/li&gt;
&lt;li&gt;BO에서 Bean 이름, 메서드명, Cron 표현식을 등록하면 서버 기동 시 스케줄이 자동으로 로드되어 실행됩니다. 수동 실행도 BO에서 HTTP 호출로 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 배치는 어떻게 실행되는가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 시스템은 Quartz가 아니라 Spring ThreadPoolTaskScheduler 기반의 커스텀 스케줄러를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;운영자는 BO에서 Bean 이름, Method 이름, Cron 표현식을 등록할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;서버가 기동되면 DB에 저장된 스케줄 목록을 읽어 자동 등록한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한 Cron, Fixed Delay, Fixed Rate 방식을 운영 중에도 변경할 수 있으며 별도 배포가 필요 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadPoolTaskScheduler를 사용하기 때문에 여러 배치가 동시에 실행될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ThreadPoolTaskScheduler&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;배치 A 실행 중, 배치 B 동시 실행 중, 배치 C 동시 실행 중&lt;/li&gt;
&lt;li&gt;직원이 여러명인 회사라고 볼 수 있음&lt;/li&gt;
&lt;li&gt;배치가 겹쳐도 밀리지 않음.&lt;/li&gt;
&lt;li&gt;쓰레드를 여러 개 운영중&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 배치가 정말 다른 배치에 영향을 주지 않을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 A 배치 실패 &amp;rarr; B 배치 영향 없음이라고 생각했다.&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;&lt;span&gt;배치는 독립적으로 실행되더라도 데이터는 공유한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 A 배치가 상태값을 READY &amp;rarr; SEND 로 변경하던 중 실패한다면 READY 데이터가 남을 수 있다.&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;&lt;span&gt;결국 중요한 것은 스케줄러가 아니라 데이터의 상태 전이와 멱등성 보장 방식이었다.&lt;/span&gt;&lt;/p&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ThreadPoolTaskScheduler기반이기 때문에 배치 간 독립적으로 실행&lt;/li&gt;
&lt;li&gt;A 배치 실패 -&amp;gt; B 배치 영향 X&lt;/li&gt;
&lt;li&gt;예외 케이스
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;DB를 공유하는 경우!
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;A배치가 B테이블 쓰다가 실패&lt;/li&gt;
&lt;li&gt;다음 배치 실행 시 READY가 남아있음&lt;/li&gt;
&lt;li&gt;중복 처리 가능성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 배포 중 실행 중인 배치는 어떻게 될까?&lt;/b&gt;&lt;/h3&gt;
&lt;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;&lt;span&gt;실제로 배포 스크립트를 확인해보니 그렇지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;배포 과정에서 먼저 ShutdownSigTerm API를 호출한다.&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;&lt;span&gt;작업 완료 후에만 WAS를 종료한다.&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;&lt;span&gt;또한 batch-01, batch-02 두 대를 순차적으로 배포하는 롤링 배포 구조를 사용하고 있어 배포 중에도 항상 한 대는 운영 상태를 유지한다.&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;장애 시나리오 추론&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;서버 재시작되면 스케줄이 초기화되고 DB에서 다시 로드되는 구조
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;서버 재기동 시 실행 중이던 스케줄 상태를 초기화하고, DB에 등록된 사용 중인 배치 목록을 다시 로드해 자동으로 재등록함.&lt;/li&gt;
&lt;li&gt;배포 후 별도 작업 없이도 스케줄이 자동으로 살아남.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;배포 중 실행 중이던 배치는 어떻게 되는지
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;.gitlab/prd/.prd-batch.yml 파일에서 확인해본 결과,&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;batch_health_check -&amp;gt; 실행 중인 배치 완료 대기&lt;/li&gt;
&lt;li&gt;WAS Stop&lt;/li&gt;
&lt;li&gt;WAS Start&lt;/li&gt;
&lt;li&gt;크게 빌드 -&amp;gt; 스크립트 배포 -&amp;gt; WAS 배포(2단계) -&amp;gt; 완료 알림 흐름&lt;/li&gt;
&lt;li&gt;대상 서버는 batch-01, batch-02 두 대, 순차 처리 로직&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;네트워크나 장애가 발생했을때는?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이미지 배치의 경우 READY &amp;rarr; SEND &amp;rarr; SUCCESS/FAILED 상태 전이 모델로 멱등성을 보장했습니다. 반면 기존 일반 배치들은 주기적으로 실행되는 구조상, 장애 발생 시 해당 실행분은 누락되더라도 다음 스케줄 실행 시 정상 처리되도록 설계하였습니다. 다만 데이터 정합성이 중요한 배치의 경우 상태 전이 모델 적용이 필요하다고 판단하고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;10:build:batch:prd
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배치 WAR 빌드 단계 (.prd_gradle_build_helper 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;20:deploy:batch:prd:script
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에 운영 스크립트(batch-script.tgz) 배포/압축해제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;40:deploy:batch:prd:auto
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 배포 시작용 게이트(메시지/흐름 연결 역할)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;44:deploy:batch:prd:was 0/1
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WAS 재기동 없이 먼저 WAR/Exploded 배포 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;44:deploy:batch:prd:was 1/1
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배치 active 작업 확인 후 WAS stop/start 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;90:deploy:batch:send:done
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최종 Slack 성공 알림&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;배포 시 서버를 즉시 내리지 않고, 먼저 ShutdownSigTerm API를 호출해 실행 중인 배치가 완료될 때까지 대기.&lt;/li&gt;
&lt;li&gt;완료 확인 후 WAS를 종료하기 때문에 배치가 중간에 강제 중단 X&lt;/li&gt;
&lt;li&gt;batch-01과 batch-02를 순차적으로 배포하는 롤링 배포 구조, 배포 중에도 한 대는 항상 운영&lt;/li&gt;
&lt;li&gt;배치마다 실행 서버를 지정할 수 있어, 운영 중 배치 중단 리스크를 최소화할 수 있는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;레거시(IDC) &amp;rarr; 클라우드 마이그레이션 과도기 시스템에서 개발한 경험&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kafka가 IDC에 있고, order_api는 Cloud에 있음&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;즉 Cloud(order_api) &amp;rarr; IDC(Kafka) 로 메시지를 던지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;크로스 네트워크 구조&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이 연결이 끊기면 어떻게 되는지 설명할 수 있으면 강점&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis가 cache / session 분리&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;마무리&lt;/span&gt;&lt;/b&gt;&lt;/h3&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;&lt;span&gt;예전에는 &quot;배치가 성공했는지&quot;만 확인했다.&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;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;배치는 어디서 실행되는가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;장애가 발생하면 어떤 시스템이 영향을 받는가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;배포 중 실행 중인 배치는 어떻게 처리되는가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;네트워크가 끊기면 어떤 문제가 발생하는가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;데이터 중복은 어떻게 방지하는가?&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  개발 성장 로그</category>
      <category>Batch</category>
      <category>개발자</category>
      <category>면접대비</category>
      <category>배치서버</category>
      <category>백엔드</category>
      <category>인프라</category>
      <author>누스타악</author>
      <guid isPermaLink="true">https://nu-stock.tistory.com/31</guid>
      <comments>https://nu-stock.tistory.com/31#entry31comment</comments>
      <pubDate>Tue, 2 Jun 2026 13:35:32 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 면접 단골 질문 - 커버링 인덱스는 왜 빠를까?</title>
      <link>https://nu-stock.tistory.com/30</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;커버링 인덱스는 논클러스터드 인덱스의 특징인 Random I/O를 없애버리는 기법이라, 한번 봐두면 좋다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &lt;b&gt;논클러스더트 인덱스 내용 참조&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nu-stock.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nu-stock.tistory.com/29&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1780318520600&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;[DB] 클러스터드 인덱스 vs 논클러스터드 인덱스, Oracle에는 왜 없을까?&quot; data-og-description=&quot;면접 준비하자! 하면 DB 내용 중에 많이 나오는 것 중 하나인 클러스터드 인덱스와 논클러스터드 인덱스!근데 왜 알아야 하냐면 대부분의 테크기업에는 Mysql이나 PostgreSQL을 사용하기 때문에 oracle&quot; data-og-host=&quot;nu-stock.tistory.com&quot; data-og-source-url=&quot;https://nu-stock.tistory.com/29&quot; data-og-url=&quot;https://nu-stock.tistory.com/29&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5qG0N/dJMb9fZCpKL/QK54A3yqP96gkEcGVaHXTK/img.png?width=800&amp;amp;height=352&amp;amp;face=0_0_800_352,https://scrap.kakaocdn.net/dn/7KGG7/dJMb9b3Y7xx/DJGcWDlVTl8eDNWOGpzMKK/img.png?width=800&amp;amp;height=352&amp;amp;face=0_0_800_352,https://scrap.kakaocdn.net/dn/06SPP/dJMb9bv9mrE/ckCWKKekAB41lLxzf9MCsK/img.png?width=1026&amp;amp;height=452&amp;amp;face=0_0_1026_452&quot;&gt;&lt;a href=&quot;https://nu-stock.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nu-stock.tistory.com/29&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5qG0N/dJMb9fZCpKL/QK54A3yqP96gkEcGVaHXTK/img.png?width=800&amp;amp;height=352&amp;amp;face=0_0_800_352,https://scrap.kakaocdn.net/dn/7KGG7/dJMb9b3Y7xx/DJGcWDlVTl8eDNWOGpzMKK/img.png?width=800&amp;amp;height=352&amp;amp;face=0_0_800_352,https://scrap.kakaocdn.net/dn/06SPP/dJMb9bv9mrE/ckCWKKekAB41lLxzf9MCsK/img.png?width=1026&amp;amp;height=452&amp;amp;face=0_0_1026_452');&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;[DB] 클러스터드 인덱스 vs 논클러스터드 인덱스, Oracle에는 왜 없을까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;면접 준비하자! 하면 DB 내용 중에 많이 나오는 것 중 하나인 클러스터드 인덱스와 논클러스터드 인덱스!근데 왜 알아야 하냐면 대부분의 테크기업에는 Mysql이나 PostgreSQL을 사용하기 때문에 oracle&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nu-stock.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 쿼리를 돌린다고 가정해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1780318898578&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT name, email FROM members WHERE name = '김철수'&lt;/code&gt;&lt;/pre&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;이름 인덱스(B+tree)를 타고 내려가 '김철수' 위치를 찾음&lt;/li&gt;
&lt;li&gt;근데 email은 인덱스에 없으니 그 위치로 점프해서 테이블 본체를 읽어 email을 가져옴 (점프가 random I/O)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 2번 점프를 Oracle에서는 &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;테이블 액세스(table access by index rowid)&lt;/b&gt;&lt;/span&gt;라고 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스에 없는 컬럼을 가지러 테이블 본체까지 갔다 옴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커버링 인덱스는 이 점프를 아예 없애준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rtJfh/dJMcag6MeAS/dkaVyRoVzJxjvyYkDjvFU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rtJfh/dJMcag6MeAS/dkaVyRoVzJxjvyYkDjvFU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rtJfh/dJMcag6MeAS/dkaVyRoVzJxjvyYkDjvFU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrtJfh%2FdJMcag6MeAS%2FdkaVyRoVzJxjvyYkDjvFU1%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;773&quot; height=&quot;364&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;502&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;그 쿼리가 필요로 하는 컬러을 인덱스 안에 전부 담아둬서, 테이블 본체를 안 봐도 인덱스만으로 답이 끝나는 인덱스&lt;/li&gt;
&lt;li&gt;이름의 직관 = 인덱스가 그 쿼리를 덮는다(= cover)&lt;/li&gt;
&lt;li&gt;인덱스에 (name, email)을 같이 인덱스에 넣는다면 테이블로 점프할 일이 없음&lt;/li&gt;
&lt;li&gt;조회 건수가 많을 수록 점프 1000번이 0번이 되니까 효과가 큼&lt;/li&gt;
&lt;li&gt;테이블 액세스를 제거한다는 큰 장점!!&lt;/li&gt;
&lt;/ul&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인덱스가 뚱뚱~
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;굳이 name만 있으면 되는데, email까지 넣어서 인덱스 크기가 커짐&lt;/li&gt;
&lt;li&gt;욕심내서 컬럼을 5개, 10개 넣으면 인덱스가 거의 테이블만큼 커져서, 인덱스를 읽는 것 자체가 느려짐.&lt;/li&gt;
&lt;li&gt;저장공간, 메모리도 더 먹음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;쓰기가 더 어려워짐
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;email이 바뀌면 테이블뿐 아니라 인덱스도 같이 갱신 (인덱스의 일반적인 trade-off)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;자주 같이 조회되는 소수의 컬럼&lt;/b&gt;일 때만 커버링으로 묶기&lt;/li&gt;
&lt;li&gt;DB별 차이
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Oracle
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;그냥 인덱스에 컬럼을 다 포함시키면 옵티마이저가 알아서 테이블 액세스를 생략&lt;/li&gt;
&lt;li&gt;실행계획에 테이블 액세스 단계가 사라지는 걸로 확인하면 됨!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MySQL InnoDB
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;똑같이 동작하는데, 실행계획에서 Using index 라고 뜨면 커버링이 적용됐다~&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  개발 성장 로그</category>
      <category>db</category>
      <category>개발자</category>
      <category>면접대비</category>
      <category>백엔드</category>
      <category>인덱스</category>
      <category>커버링인덱스</category>
      <author>누스타악</author>
      <guid isPermaLink="true">https://nu-stock.tistory.com/30</guid>
      <comments>https://nu-stock.tistory.com/30#entry30comment</comments>
      <pubDate>Mon, 1 Jun 2026 22:48:25 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 클러스터드 인덱스 vs 논클러스터드 인덱스, Oracle에는 왜 없을까?</title>
      <link>https://nu-stock.tistory.com/29</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;면접 준비하자! 하면 DB 내용 중에 많이 나오는 것 중 하나인 클러스터드 인덱스와 논클러스터드 인덱스!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 왜 알아야 하냐면 대부분의 테크기업에는 Mysql이나 PostgreSQL을 사용하기 때문에 oracle만 사용했던 나같은 사람들도 좀 기억을 해두는 게 좋을 것 같다!&lt;/p&gt;
&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;Oracle을 쓰면 만날 일이 없다는 이녀석 개념은 알아두자&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;클러스터드 vs 논클러스터드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;핵심 질문 : 인덱스 순서대로, 실제 데이터(테이블 행)도 그 순서로 디스크에 줄 세워 저장하느냐?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;YES : 클러스터드&lt;/li&gt;
&lt;li&gt;NO : 논클러스터드&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tOqmN/dJMcabEnx4p/H35fslV66fRLtqkDXvpPW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tOqmN/dJMcabEnx4p/H35fslV66fRLtqkDXvpPW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tOqmN/dJMcabEnx4p/H35fslV66fRLtqkDXvpPW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtOqmN%2FdJMcabEnx4p%2FH35fslV66fRLtqkDXvpPW0%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;756&quot; height=&quot;333&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;452&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인덱스 순서 = 데이터 저장 순서&lt;/li&gt;
&lt;li&gt;ID로 클러스터드 인덱스를 만들면, 테이블 행 자체가 ID 1,2,3,4.. 순서로 디스크에 줄세워 저장됨.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인덱스가 곧 데이터&lt;/li&gt;
&lt;li&gt;ID로 찾아 내려가면 그 리프 자리에 데이터가 바로 있음 (한번에 끝)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장점&lt;/li&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;찾으면 데이터가 거기 있으니 추가 점프 X&lt;/li&gt;
&lt;li&gt;특히 범위 검색 (ID 100 ~ 200)이 빠름&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;sequential I/O&lt;/b&gt;&lt;/span&gt;로 쭉 읽으면 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;li&gt;한계
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터를 물리적으로 한 줄로 세우는 거라 기준은 하나뿐!&lt;/li&gt;
&lt;li&gt;테이블당 클러스터드 인덱스는 딱 1개
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ex) 데이터를 날짜순으로 인쇄하면 가격순으로는 인쇄할 수 없는거랑 같음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인덱스는 데이터의 위치(주소)만 들고 있고, 실제 데이터는 다른 곳에 저장&lt;/li&gt;
&lt;li&gt;이름순으로 논클러스터드 인덱스를 만들면, 인덱스엔 &quot;김철수 -&amp;gt; 2번 위치&quot;식으로 주소만 있고, 행을 읽으려면 주소로 한 번 더 점프&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터 본체랑 분리돼 있으니 &lt;b&gt;여러개 만들 수 있음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이름순, 가격순, 날짜순 인덱스를 동시에 둘 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;한계
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인덱스에서 위치 찾고 -&amp;gt; 데이터로 점프하는 2단계라 클러스터드보다 한번 더 일함&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;random I/O&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Oracle 개발자는 몰라?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Oracle&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;기본 테이블이 Heap 테이블 : 데이터를 그냥 빈자리에 넣음 (정렬 x)&lt;/li&gt;
&lt;li&gt;모든 인덱스는 사실상 논클러스터드처럼 동작&lt;/li&gt;
&lt;li&gt;데이터를 인덱스 순으로 정렬 저장하고 싶으면 &quot;&lt;b&gt;IOT(Index-Organized Table)&lt;/b&gt;&quot;라는 걸 따로 만들어야 함
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Oracle식 클러스터드 인덱스&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MySQL(InnoDB)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;무조건 클러스터드가 강제&lt;/li&gt;
&lt;li&gt;PK가 자동으로 클러스터드 인덱스가 되고, 데이터가 PK 순서로 정렬 저장됨&lt;/li&gt;
&lt;li&gt;그래서 InnoDB에선 PK 설계가 성능에 직접 영향을 줌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클러스터드는 인덱스 순서대로 데이터 자체를 정렬 저장해서 인덱스가 곧 데이터인 방식이고, 테이블당 하나만 가능합니다. 논클러스터드는 위치만 가리켜서 여러 개 만들 수 있고요. 다만 이건 SQL Server/MySQL 용어이고, 제가 쓰던 Oracle은 기본이 힙 테이블이라 이 개념이 없고, 대신 IOT가 클러스터드에 해당합니다. MySQL InnoDB는 반대로 PK가 항상 클러스터드입니다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  개발 성장 로그</category>
      <author>누스타악</author>
      <guid isPermaLink="true">https://nu-stock.tistory.com/29</guid>
      <comments>https://nu-stock.tistory.com/29#entry29comment</comments>
      <pubDate>Mon, 1 Jun 2026 21:53:40 +0900</pubDate>
    </item>
    <item>
      <title>[웹] CORS는 왜 존재할까? 브라우저는 왜 내 API 호출을 막을까?</title>
      <link>https://nu-stock.tistory.com/28</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서 CORS 에러가 나니까, 이건 백엔드가 아니고 프론트엔드 문제라고 나는 생각해왔기 때문에 깊게 살펴보지도 공부하지도 않았었다. 근데 백엔드 면접에서 계속 나오더라.. 그러니 한번 봐보자.&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;CORS란?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Cross-Origin Resource Sharing&lt;/li&gt;
&lt;li&gt;보안 기능이 아니라 브라우저의 보안 정책을 &quot;완화&quot;하는 장치&lt;/li&gt;
&lt;li&gt;문제가 아니라 허용해주는 규약!!&lt;/li&gt;
&lt;li&gt;브라우저에는 원래 동일 출처 정책(Same-Origin Policy, SOP)이 있음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;출처(Origin) = 프로토콜 + 호스트 + 포트, 세 가지가 모두 같아야 같은 출처
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;https://shop.com &amp;rarr; https://&lt;span style=&quot;color: #ef6f53;&quot;&gt;api&lt;/span&gt;.shop.com : &lt;b&gt;호스트&lt;/b&gt; 다름 &amp;rarr; 다른 출처&lt;/li&gt;
&lt;li&gt;https://shop.com &amp;rarr; &lt;span style=&quot;color: #ef6f53;&quot;&gt;http&lt;/span&gt;://shop.com : &lt;b&gt;프로토콜&lt;/b&gt; 다름 &amp;rarr; 다른 출처&lt;/li&gt;
&lt;li&gt;https://shop.com:443 &amp;rarr; https://shop.com:&lt;span style=&quot;color: #ef6f53;&quot;&gt;8080&lt;/span&gt; : &lt;b&gt;포트&lt;/b&gt; 다름 &amp;rarr; 다른 출처&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SOP는 &quot;A 사이트에서 받은 스크립트가 B 사이트의 응답을 함부로 읽지 못하게&quot; 막음&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;CORS는 거꾸로 &quot;B 서버가 명시적으로 허락하면 A에서 읽어도 돼&quot;라고 &lt;b&gt;풀어주는&lt;/b&gt; 규약&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;만약 SOP가 없다면, 사용자가 로그인된 상태로 악성 사이트에 접속했을 때 그 사이트의 자바스크립트가 사용자 쿠키를 실어서 bank.com/내계좌를 호출하고 &lt;b&gt;응답 내용을 읽어가는&lt;/b&gt; 일이 가능
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SOP가 이 응답을 읽는 행위를 막음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CORS 위반이어도 요청은 서버에 도달할 수 있음.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;브라우저는 &quot;응답을 자바스크립트에게 넘겨주는 단계&quot;에서 막을 뿐, 요청 전송 자체를 항상 막는 건 아님&lt;/li&gt;
&lt;li&gt;CORS는 어디까지나 &lt;b&gt;브라우저가 강제하는 클라이언트 측 정책&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;CORS에러는 서버가 허용 헤더를 안 내려줘서 생기는 거라, 해결도 보통 서버쪽에서 함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버에서 응답 헤더 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Access-Control-Allow-Origin에 허용할 출처를 명시&lt;/li&gt;
&lt;li&gt;Spring이라면 @CrossOrigin, 또는 전역으로 WebMvcConfigurer의 addCorsMappings, 시큐리티를 쓰면 CorsConfigurationSource로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인증 정보(쿠키)를 같이 보낼 때&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ccess-Control-Allow-Credentials: true가 필요하고, 이때는 Allow-Origin에 *를 쓸 수 없음&lt;/li&gt;
&lt;li&gt;반드시 구체적인 출처를 명시해야 함. (보안상 와일드카드 + 쿠키 조합을 브라우저가 막아둠)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프록시 우회&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;개발 환경에선 같은 출처처럼 보이게 하는 프록시(Vite/webgpack dev server proxy, nginx 리버스 프록시)로 푸는 경우도 많음&lt;/li&gt;
&lt;li&gt;출처를 같게 만들어 SOP 자체를 안 건드리는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;매번 OPTIONS 요청이 추가로 나가면 비효율 아닌가요?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;맞습니다. 그래서 Access-Control-Max-Age로 예비 요청 결과를 캐싱해 일정 시간 동안 재요청을 생략합니다. 트레이드오프는 캐시시간과 정책 변경 반영 지연이 생길 수 있으므로 상황에 맞춰서 변경할 수 있을 것이라 생각합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780211956444&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app:
  cors:
    # 허용할 프론트 출처(콤마 구분). 예: http://localhost:3000,http://localhost:5173
    allowed-origins: http://localhost:3000&lt;/code&gt;&lt;/pre&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;852&quot; data-origin-height=&quot;726&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLCfLI/dJMcaiQ1Y2W/1DHHpA9C4gKVjK8yMLitk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLCfLI/dJMcaiQ1Y2W/1DHHpA9C4gKVjK8yMLitk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLCfLI/dJMcaiQ1Y2W/1DHHpA9C4gKVjK8yMLitk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLCfLI%2FdJMcaiQ1Y2W%2F1DHHpA9C4gKVjK8yMLitk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;591&quot; height=&quot;504&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;726&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;Config를 Spring이 부르는 방법&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;addCorsMappings를 부르는 게 아니라, 스프링이 대신 불러줌 = 제어의 역전(IoC)&lt;br /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@Configuration이 붙어 있으니 앱이 뜰 때 컴포넌트 스캔이 이 클래스를 발견하고 빈으로 등록&lt;/li&gt;
&lt;li&gt;빈을 만들려면 생성자를 호출&lt;/li&gt;
&lt;li&gt;생성자에 @Value가 있으니 이 시점에 yml 값이 allowedOrigins로 주입&lt;/li&gt;
&lt;li&gt;이 클래스가 WebMvcconfigurer를 구현&lt;/li&gt;
&lt;li&gt;스프링 MVC 설정부(DelegatiogWebMvcConfiguration 쪽)는 컨테이너에 &quot;WebMvcConfigurer&quot;타입 빈 전부 다 줘&quot;라고 요청해서 리스트로 모음
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;DelegatiogWebMvcConfiguration : 위임(delegation)하는 중계자 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MVC를 세팅하는 과정에서 모아둔 configurer들을 하나씩 돌면서 addCorsMappings(registry)를 직접 호출&lt;/li&gt;
&lt;li&gt;이때 registry도 스프링이 만들어서 인자로 넘겨줌&lt;/li&gt;
&lt;li&gt;따라서 코드 어디에도 호출문이 없는 게 정상! (호출하는 주체가 코드가 아니라 프레임워크)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780212149278&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 전역 CORS 설정.
 *
 * 프론트(gb-web, http://localhost:3000)가 브라우저에서 직접 백엔드를 호출할 수 있도록 허용한다.
 * 허용 출처는 application.yml 의 `app.cors.allowed-origins` 로 외부화(콤마 구분, 기본값 localhost:3000).
 *
 * 현재 프론트는 쿠키/세션 인증을 사용하지 않으므로 allowCredentials 는 켜지 않는다.
 */
@Configuration
class WebConfig(
    @Value(&quot;\${app.cors.allowed-origins:http://localhost:3000}&quot;)
    private val allowedOrigins: Array&amp;lt;String&amp;gt;,
) : WebMvcConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping(&quot;/api/**&quot;)
            .allowedOrigins(*allowedOrigins)
            .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;PATCH&quot;, &quot;DELETE&quot;, &quot;OPTIONS&quot;)
            .allowedHeaders(&quot;*&quot;)
            .maxAge(3600)
    }
}&lt;/code&gt;&lt;/pre&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;700&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmC4uN/dJMcadPGqHj/qzIj4DMn7W3gS64FWKJ2Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmC4uN/dJMcadPGqHj/qzIj4DMn7W3gS64FWKJ2Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmC4uN/dJMcadPGqHj/qzIj4DMn7W3gS64FWKJ2Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmC4uN%2FdJMcadPGqHj%2FqzIj4DMn7W3gS64FWKJ2Wk%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;523&quot; height=&quot;523&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>  개발 성장 로그</category>
      <category>CORS</category>
      <category>Java</category>
      <category>Kotlin</category>
      <category>개발자면접</category>
      <category>백엔드면접</category>
      <author>누스타악</author>
      <guid isPermaLink="true">https://nu-stock.tistory.com/28</guid>
      <comments>https://nu-stock.tistory.com/28#entry28comment</comments>
      <pubDate>Sun, 31 May 2026 16:46:18 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처] 비즈니스 로직은 Service에 둘까, Entity에 둘까? (DDD)</title>
      <link>https://nu-stock.tistory.com/27</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;DDD가 대체 뭘까..?&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;DDD&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;소프트웨어를 기술이 아니라 업무(도메인) 중심으로 모델링하자.&lt;/li&gt;
&lt;li&gt;그 서비스가 다루는 실제 업무가 설계를 이끌게 하자.&lt;/li&gt;
&lt;li&gt;코드가 DB 구조가 아니라 비즈니스가 생각하는 방식을 닮아야 한다는 철학&lt;/li&gt;
&lt;li&gt;기둥1
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;유비쿼터스 언어(공용어)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;개발자, 기획자/도메인 전문가가 같은 단어를 쓰고, 그 단어가 코드에 그대로 나타나야 함.&lt;/li&gt;
&lt;li&gt;장바구니에 담는다 -&amp;gt; cart.addItem() O, cartService.insertCartRow() X&lt;/li&gt;
&lt;li&gt;기획이 원하는 것과 코드가 원하는 것 사이의 번역 오류가 줄어드는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기둥2
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;풍부한 도메인 모델
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;DDD는 규칙(행위)이 데이터와 같은 객체 안에 살아야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780202667792&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 풍부한 모델 (DDD가 지향) &amp;mdash; 규칙이 객체 안에
class Cart {
    private List&amp;lt;Item&amp;gt; items;
    void addItem(Item item) {
        if (item.isRefrigerated() &amp;amp;&amp;amp; refrigeratedCount() &amp;gt;= 5)
            throw new CartRuleException(&quot;냉장 상품은 5개까지 담을 수 있습니다&quot;);
        items.add(item);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;DDD의 진짜 핵심은 패턴 암기가 아니라 &quot;업무를 대화로 이해해서 모델로 옮기는 규율&quot;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;헥사고날은 도메인을 깨끗하게 격리하는 '그릇&amp;middot;구조'를 주고, DDD는 그 깨끗한 도메인 안을 '업무를 닮은 풍부한 모델'로 채우는 방법&lt;/li&gt;
&lt;li&gt;레이어 vs 헥사고날 그리고 DDD&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKPuL1/dJMcab5sAXT/ycVwKGhbZ8BlXSQyI1OHKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKPuL1/dJMcab5sAXT/ycVwKGhbZ8BlXSQyI1OHKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKPuL1/dJMcab5sAXT/ycVwKGhbZ8BlXSQyI1OHKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKPuL1%2FdJMcab5sAXT%2FycVwKGhbZ8BlXSQyI1OHKK%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;702&quot; height=&quot;284&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;420&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;나는 여태까지 DDD에 나오는 도메인이 곧 DTO를 나타내는 것이라고 생각했는데, 뭔가 잘못된 생각이라는 걸 이제야 깨닫게 됐다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;DDD는 객체지향의 원래 정신&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;OOP는 원래 &quot;데이터와 그 데이터를 다루는 행위를 한 객체에 같이 두자&quot;였는데, 우리가 습관적으로 데이터(Entity)와 행위(Service)를 갈라놨던 것.&lt;/li&gt;
&lt;li&gt;그러니까 빈약한 모델이 오히려 객체지향을 절차지향처럼 쓴 거고, 풍부한 모델/DDD는 그걸 제자리로 돌려놓는 것.&lt;/li&gt;
&lt;li&gt;&quot;DTO에 로직 넣는 이상한 짓&quot;이 아니라 &quot;객체에 원래 있어야 할 걸 돌려준 것&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780203235439&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ① 진짜 DTO &amp;mdash; 웹에서 들어온 데이터 운반만. 규칙 없음
class AddItemRequest {
    Long productId;
    int quantity;          // getter/setter만
}

// ② 도메인 객체(엔티티) &amp;mdash; 데이터 + '자기 규칙'을 가짐. DTO 아님
class Cart {
    private List&amp;lt;Item&amp;gt; items;
    void addItem(Item item) {                 // 카트 하나로 판단되는 규칙
        if (item.isRefrigerated() &amp;amp;&amp;amp; refrigeratedCount() &amp;gt;= 5)
            throw new CartRuleException(&quot;냉장 상품은 5개까지&quot;);
        items.add(item);
    }
}

// ③ Service &amp;mdash; '규칙'이 아니라 '조율'을 함
class CartService {
    void addItem(Long cartId, AddItemRequest req) {
        Cart cart = cartStore.findById(cartId);   // ㉠ DB에서 꺼내고 (바깥일)
        Item item = itemFactory.from(req);
        cart.addItem(item);                        // ㉡ 규칙은 Cart에게 맡김
        cartStore.save(cart);                      // ㉢ 다시 저장 (바깥일)
    }
}&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;b&gt;DDD 적용해봤나요?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;객체에 validation 메서드를 두긴 했지만, setter가 열려 있어 상태를 외부에서 바꿀 수 있었고 검증을 따로 호출하는 방식이었습니다. 그래서 객체가 스스로 불변식을 지키는 풍부한 도메인 모델까지는 아니었고, 입력 검증을 객체에 모아둔 정도였다고 생각합니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  개발 성장 로그</category>
      <category>ddd</category>
      <category>도메인</category>
      <category>면접대비</category>
      <category>백엔드</category>
      <category>아키텍처</category>
      <author>누스타악</author>
      <guid isPermaLink="true">https://nu-stock.tistory.com/27</guid>
      <comments>https://nu-stock.tistory.com/27#entry27comment</comments>
      <pubDate>Sun, 31 May 2026 14:24:17 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Controller-Service-Repository 구조의 한계, 헥사고날 아키텍처는 무엇이 다를까?</title>
      <link>https://nu-stock.tistory.com/26</link>
      <description>&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;&lt;b&gt;&quot;너 탈락&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 탈락하지 않아야겠다는 의지로.. 아키텍처 체크 한번 해보자..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;계층형 (Layered / 3-tier)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Controller &amp;rarr; Service &amp;rarr; Repository &amp;rarr; DB&lt;/li&gt;
&lt;li&gt;핵심 아이디어 : 기술적 관심사로 나눈다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;화면/비즈니스/데이터 접근을 층으로 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실무에서는 Service 계층에 비즈니스 규칙과 영속성 로직이 함께 모이는 경우가 쉬움&lt;/li&gt;
&lt;li&gt;규모가 커질수록 Service가 비대해지고, 도메인 모델은 단순 데이터 컨테이너로 전락하기 쉬움&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의존성 방향&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;계층형은 의존성이 위에서 아래로, 즉 DB쪽으로 향함.&lt;/li&gt;
&lt;li&gt;Service가 Repository를 알고, Repository가 JPA/DB를 안다.&lt;/li&gt;
&lt;li&gt;결국 비즈니스 로직이 영속성 기술에 묶임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장점 : 단순하고 직관적이고, 모든 개발자가 알고, Spring 기본 구조와 일치해서 시작이 빠름&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;(트레이드오프) 도메인 로직이 갈 곳이 없어서 전부 Service로 몰림&lt;/li&gt;
&lt;li&gt;도메인 객체는 getter/setter만 있는 빈약한 도메인 모델이 되기 쉬움.&lt;/li&gt;
&lt;li&gt;비즈니스 규칙을 테스트하려면 DB/JPA를 같이 띄워야 해서 순수 단위 테스트 어려움&lt;/li&gt;
&lt;li&gt;규모가 커지면 Service가 god class가 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;헥사고날 (Ports &amp;amp; Adapters)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이번에 처음으로 계층 아키텍처에서 헥사고날 아키텍처를 사용해보면서 정확한 비교가 필요하다고 생각했다.&lt;/li&gt;
&lt;li&gt;헥사고날은 DDD를 구현할 때 자주 사용되는 아키텍처 중 하나&lt;/li&gt;
&lt;li&gt;계층형과 정반대로, 모든 의존성이 도메인(중심)쪽으로 향함 = 모든 소스 코드 의존성이 도메인 또는 애플리케이션 계층을 향하도록 설계&lt;/li&gt;
&lt;li&gt;도메인이 포트(인터페이스)를 정의&lt;/li&gt;
&lt;li&gt;바깥의 어댑터(JPA, Rest, Kafka)가 그 포트를 구현
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;도메인은 JPA가 뭔지, Kafka가 뭔지 모름.&lt;/li&gt;
&lt;li&gt;이게 &lt;b&gt;의존성 역전(DIP)&lt;/b&gt;의 실제 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인바운드 어댑터(Controller, 메시지 리스너) -&amp;gt; 유스케이스 포트를 호출&lt;/li&gt;
&lt;li&gt;아웃바운드 어댑터(JPA 레포지토리, 외부 API 클라이언트) -&amp;gt; 도메인이 정의한 포트를 구현&lt;/li&gt;
&lt;li&gt;인터페이스로 화살표를 뒤집는 작업이, 이 &quot;묶임&quot;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;푸는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;행위&lt;/li&gt;
&lt;li&gt;규칙을 JPA 표식 없는 순수한 객체로 떼어내면, 규칙은 더 이상 영속성 기술에 묶이지 않음&lt;/li&gt;
&lt;li&gt;장점 : 도메인이 인프라 없이 순수하게 테스트되고, DB나 메시징을 갈아끼우기 쉽고, 경계가 명시적, 복잡하고 오래 갈 시스템에 강함&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;보일러플레이트가 많아짐&lt;/li&gt;
&lt;li&gt;인터페이스가 늘어남&lt;/li&gt;
&lt;li&gt;도메인 객체 &amp;lt;-&amp;gt; JPA 엔티티 &amp;lt;-&amp;gt; DTO 매핑 비용이 늘어남&lt;/li&gt;
&lt;li&gt;단순 CRUD에 적용하면 과설계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qjDq8/dJMcabLdIgS/nJpDVPL6k47HMnnfRfUI60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qjDq8/dJMcabLdIgS/nJpDVPL6k47HMnnfRfUI60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qjDq8/dJMcabLdIgS/nJpDVPL6k47HMnnfRfUI60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqjDq8%2FdJMcabLdIgS%2FnJpDVPL6k47HMnnfRfUI60%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;590&quot; height=&quot;374&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;430&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;무엇이 무엇에 의존하게 둘 것인가?&lt;/li&gt;
&lt;li&gt;어떤 시스템이든 두 종류의 코드가 섞여 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;잘 안 바뀌고 진짜 중요한 것 - 비즈니스 규칙
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ex) 주문 금액이 3만원 이상이면 무료배송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자주 바뀌고 사실 부수적인 것 - 도구
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;DB가 MySQL이냐 Mongo냐&lt;/li&gt;
&lt;li&gt;JPA를 쓰냐 MyBatis를 쓰냐&lt;/li&gt;
&lt;li&gt;화면이 웹이냐 앱이냐 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;좋은 아키텍처의 목표
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;중요한 것(규칙)이 부수적인 것(도구)에 의존하지 않게 만드는 것.&lt;/li&gt;
&lt;li&gt;계층형 : Service가 규칙을 들고 있는데, 그 Service가 Repository(=JPA, DB)에 의존
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;즉, 중요한 규칙이 부수적인 도구 쪽을 바라봄&lt;/li&gt;
&lt;li&gt;DB나 ORM을 바꾸면 규칙 코드까지 건드려야 함.&lt;/li&gt;
&lt;li&gt;규칙만 테스트하고 싶어도 DB를 띄워야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;헥사고날은 이 화살표를 뒤집은 것!
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;규칙(도메인)을 한 가운데 두고, DB/web/외부API는 전부 바깥에서 정의한 인터페이스에 맞추게 함.&lt;/li&gt;
&lt;li&gt;규칙은 JPA가 뭔지도 모름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;OrderService(=중요한 규칙) -&amp;gt; OrderRepository(=DB/JPA에 묶인 도구)&lt;/li&gt;
&lt;li&gt;화살표는 항상 필요로 하는 쪽에서 필요한 쪽으로 감&lt;/li&gt;
&lt;li&gt;화살표가 있다는 것은 뒤엣것이 바뀌면 앞엣것이 영향을 받음&lt;/li&gt;
&lt;li&gt;A가 B를 알고/쓰고/필요로 한다 = A가 B에 의존한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;계층 -&amp;gt; 헥사고날로 가는 이유&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;영속성 기술에 묶인다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;중요한 규칙 코드가 DB 저장 기술에 의존해버려서, 저장 기술이 바뀔 때마다 규칙까지 같이 흔들리는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기술에 묶이지 않기 위해서 헥사고날로 가는중!&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&quot;매핑 비용이라는 대가를 알면서도, 도메인 복잡도가 그 대가를 정당화했기 때문에 선택했다&quot;&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;매핑 : 한 모양을 다른 모양으로 바꿔주는 작업&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;도메인 객체를 순수하게 둬도 결국 @Entity 도 필요&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;결국 저장할때마다 Order -&amp;gt; OrderEntity, OrderEntity -&amp;gt; Order로 서로 바꿔주는 코드를 써야 함&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;도메인 -&amp;gt; 엔티티, 엔티티 -&amp;gt; 도메인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;도메인이 단순하면(그냥 CRUD, 규칙이 거의 없음) &amp;rarr; 보호할 알맹이가 없는데 매핑 비용만 내는 거라 &lt;b&gt;낭비, &lt;/b&gt;이럴 땐 헥사고날이 과설계.&lt;/li&gt;
&lt;li&gt;도메인이 복잡하면(장바구니처럼 추천 로직/배송 온도 조건/무료배송 판정이 얽힘) &amp;rarr; 그 복잡한 규칙을 깨끗하고 테스트 가능하게 지켜내는 가치가 &lt;b&gt;매핑 비용보다 큼,&lt;/b&gt; 그래서 비용을 내는 게 정당화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;저는 처음에 도메인이 DTO를 말하는 것인가 싶었는데!? 아니었다&lt;/li&gt;
&lt;li&gt;도메인이라는 단어가 너무 추상적이라서 그렇게 된 것&lt;/li&gt;
&lt;li&gt;도메인 : 그 서비스가 다루는 업무 영역과 그 규칙 자체.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;주문이란 무엇인가? 무료배송은 언제 되는가? 장바구니엔 뭘 담을 수 있는가? - 비즈니스의 핵심&lt;/li&gt;
&lt;li&gt;코드로는 그 규칙을 품은 객체 (Order, Cart)랑 그 규칙을 실행하는 로직.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;모든 의존성은 도메인 쪽으로 향한다&lt;/b&gt; &lt;/span&gt;라는 말은 웹 코드도, DB 코드도, 전부 도메인을 필요로 한다.(의존한다) 근데 도메인은 그 누구도 필요로 하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byHubY/dJMcadoA0o0/G2MhQy482Ss5UlbyxCp4Yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byHubY/dJMcadoA0o0/G2MhQy482Ss5UlbyxCp4Yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byHubY/dJMcadoA0o0/G2MhQy482Ss5UlbyxCp4Yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyHubY%2FdJMcadoA0o0%2FG2MhQy482Ss5UlbyxCp4Yk%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;660&quot; height=&quot;175&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;272&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인프라(Infrastructure)는 비즈니스 규칙이 아니라 그 규칙이 돌아가게 받쳐주는 기술적 기반 설비
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Oracle, AWS, Redis, Kafka&lt;/li&gt;
&lt;li&gt;만약 규칙이 인프라(JPA, DB)에 묶여 있으면, &quot;3만원 이상 무료배송&quot; 규칙 하나를 테스트하려고 해도 진짜 Oracle을 띄우고 연결해야 함.&lt;/li&gt;
&lt;li&gt;테스트가 느리고, 무겁고, DB가 잠깐 죽으면 규칙은 멀쩡한데 테스트 실패.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;OrderService(규칙)는 OrderStore라는 약속에만 의존하니까, 진짜 JPA 대신 가짜를 꽂아도 똑같이 돌아감&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Oracle도 Kafka도 안 띄우고, 순수하게 규칙만&lt;/b&gt; 검증할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;왜 헥사고날을 썼나요?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;가장 큰 이유는 복잡도 관리였습니다. 장바구니는 추천 상품 호출, 온도별 무료배송 판정, 같은 온도대 상품 추천, 가격&amp;middot;할인 계산 같은 규칙이 한 Service에 전부 몰려 있었습니다. 그 결과 Service가 너무 비대해져서, 기능 하나를 고치려 해도 코드를 분석하는 것 자체가 어려운 상태였습니다. 그래서 뒤엉킨 책임을 분리하고 핵심 규칙을 도메인으로 모으는 재구조화가 필요하다고 판단했고, 그 분리를 잡아주는 틀로 헥사고날을 적용했습니다.&lt;/li&gt;
&lt;li&gt;전체 아키텍처 방향은 팀에서 함께 잡았고, 장바구니 영역에 적용하고 설계하는 건 제가 주도했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;어떤 문제를 해결하려고 했나요?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;핵심 문제는 비즈니스 규칙과 데이터 접근이 한 Service에 뒤섞여 있다는 점이었습니다. 규칙이 여러 개 얽힌 데다 MyBatis 조회 호출까지 같은 흐름에 섞여 있어서, 어디까지가 규칙이고 어디부터가 조회 로직인지 구분이 안 됐습니다. 그래서 규칙을 도메인으로 끌어내 한곳에 모으고, 데이터 접근은 포트 뒤로 분리해 둘을 떼어놓는 것이 목표였습니다. 규칙과 인프라 호출이 섞여 코드를 이해하기 어렵다는 점이 가장 큰 문제였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그 정도면 책임 분리만으로 됐던 거 아닌가요? 굳이 헥사고날까지?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;맞는 지적입니다. 가독성 개선의 핵심은 책임 분리였고, 헥사고날은 그 분리를 강제하고 경계를 명시적으로 두기 위한 틀이었습니다. 더 가볍게 갈 수도 있었지만, 장바구니가 계속 확장되는 영역이라 명시적 경계를 두는 쪽이 장기적으로 낫다고 판단했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;계층형으로는 안됐나요? (기존꺼로는 해결이 어려웠나요?)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;계층형으로도 동작은 충분히 했을 것이고, 사실 책임 분리 자체는 계층형 안에서도 할 수 있습니다. 제가 헥사고날을 택한 이유는 도메인이 인프라를 모르게 하는 경계를 더 분명히 두고 싶었기 때문입니다. 계층형에서는 Service가 데이터 접근을 직접 들고 있어 규칙과 인프라가 다시 섞이기 쉬운데, 포트와 어댑터로 나누면 그 혼재가 눈에 띄게 됩니다. 다만 저희는 한 모듈 안에서 패키지로 나눈 수준이라 컴파일 차원의 강제까지는 아니었고, 그 점은 한계로 인정합니다. 더 강하게 가려면 도메인을 별도 모듈로 분리하거나 ArchUnit으로 검증하는 방법이 있다고 보고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;회사 프로젝트 전체에 적용해야 하나요?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;전부 적용하는 것이 답이라고 생각하지는 않습니다. 헥사고날은 도메인 객체와 영속성 모델 사이의 매핑 비용 같은 대가가 있어서, 도메인이 단순한 영역에서는 오히려 과한 구조라고 봅니다. 단순 CRUD에 가까운 영역은 계층형이 더 빠르고 읽기도 쉽습니다. 그래서 규칙이 복잡하고 오래 확장될 핵심 영역에만 선택적으로 적용하고, 단순한 영역은 계층형으로 두는 것이 합리적이라고 생각합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  개발 성장 로그</category>
      <author>누스타악</author>
      <guid isPermaLink="true">https://nu-stock.tistory.com/26</guid>
      <comments>https://nu-stock.tistory.com/26#entry26comment</comments>
      <pubDate>Sun, 31 May 2026 01:48:31 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 면접 대비 : 성별 컬럼에 B+Tree 인덱스를 걸면 왜 비효율적일까? (카디널리티, 비트맵 인덱스)</title>
      <link>https://nu-stock.tistory.com/25</link>
      <description>&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;그 컬럼에 들어 있는 값의 종류가 몇 가지냐?&lt;/li&gt;
&lt;li&gt;값이 다양하면 &lt;b&gt;높고&lt;/b&gt; 몇 종류 안 되면 &lt;b&gt;낮다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;주민번호, 이메일, 주문번호 -&amp;gt; 행마다 거의 다 다르니까 -&amp;gt; 카디널리티 높음&lt;/li&gt;
&lt;li&gt;성별(남/여), 상태값(Y/N), 배송온도(상온/냉장/냉동) -&amp;gt; 값이 2~3종류 뿐 -&amp;gt; 카디널리티 낮음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;카디널리티를 인덱스(B+tree)랑 연관지어본다면?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;사전이 빠른 건 단어가 다 다르기 때문&lt;/li&gt;
&lt;li&gt;100만 페이지짜리 사전인데 모든 단어가 '남' 아니면 '여' 둘 중 하나라고 생각해본다면?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;'여'를 찾으면 절반인 50만 개가 걸린다. (그림 참조)&lt;/li&gt;
&lt;li&gt;인덱스를 타도 거를 게 안 걸러진다!
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;인덱스의 본질&amp;nbsp;&lt;/b&gt;: 100만 개 중 몇 개로 확 좁히기!!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오히려 더 느려질 수도 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;B+tree로 50만 개를 찾으면, 그 50만 개가 테이블 여기저기 흩어져 있어서 한 건 한 건 테이블을 따로 찾아가 읽어야 함. (random I/O, 디스크 점프 50만 번)&lt;/li&gt;
&lt;li&gt;반면 풀스캔은 테이블을 처음부터 끝까지 순서대로 읽음(sequential I/O)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;디스크는 여기저기 점프보다 순서대로 쭉 읽기가 훤씬 빨라서 차라리 풀스캔이 낫다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sequential I/O (순차 읽기)&lt;/b&gt; = 통로 쭉 걷기. 디스크에서 &lt;b&gt;연속된 위치를 순서대로&lt;/b&gt; 읽는 것. 빠름&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Random I/O (임의 읽기)&lt;/b&gt; = 흩어진 책 찾아다니기. 디스크에서 &lt;b&gt;여기저기 떨어진 위치를 점프하며&lt;/b&gt; 읽는 것. 느림&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;근데 무조건 &quot;&lt;b&gt;성별 인덱스는 쓸모없다&lt;/b&gt;&quot;는 아니다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;남 99% / 여 1%일때 '여'를 찾는건 1%만 걸리니까 인덱스가 유효!&lt;/li&gt;
&lt;li&gt;이런 쏠림을 데이터 분포(히스토그램)라고 하는데, &quot;성별 인덱스 쓸모없죠?&quot; 라고 하면 &lt;b&gt;&quot;꼭 그렇진않고, 분포가 한쪽으로 쏠려 있으면.. 풀스캔보다 당연히 낫다&quot;&lt;/b&gt; 라는 걸 알려줘야 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdycTI/dJMcaccdQPM/vp6WInUIlz70bzLGIcF2Q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdycTI/dJMcaccdQPM/vp6WInUIlz70bzLGIcF2Q0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdycTI/dJMcaccdQPM/vp6WInUIlz70bzLGIcF2Q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdycTI%2FdJMcaccdQPM%2Fvp6WInUIlz70bzLGIcF2Q0%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;670&quot; height=&quot;320&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이 빈자리를 채우는 게 비트맵!!!
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;B+tree가 망하는 그 자리에 대신 쓰는 다른 종류의 인덱스 -&amp;gt; 이 문제의 해결책&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;왜 비트맵은 이게 되냐?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;B+tree는 트리 타고 내려가서 위치 찾기&lt;/li&gt;
&lt;li&gt;비트맵은 그냥 값마다 0/1 출석부 만들기.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&quot;성별=여&quot;를 찾으면 여 줄만 보면 끝. (표 참고)&lt;/li&gt;
&lt;li&gt;1이 켜진 자리 (2,5,6,8번)이 답 -&amp;gt; 트리를 탈 필요가 없어짐&lt;/li&gt;
&lt;li&gt;카디널리티랑 연결되는 핵심 -&amp;gt; 비트맵은 값 종류마다 줄을 하나씩 만듦&lt;/li&gt;
&lt;li&gt;성별 (종류 2개) -&amp;gt; 줄 2개&lt;/li&gt;
&lt;li&gt;이메일 (종류 100만 개) -&amp;gt; 줄을 100만 개 만듦.&lt;/li&gt;
&lt;li&gt;따라서 카디널리티가 낮을수록 (종류가 적을수록) 비트맵이 효율적
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;B+tree랑 정확히 정반대 성질&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;카디널리티 높음 &amp;rarr; B+tree vs 카디널리티 낮음 &amp;rarr; 비트맵&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;행 번호&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;남&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;여&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 11.1111%;&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Create Index하면 Oracle은 B+tree로 만들텐데?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;BITMAP이라는 단어 추가하면 됨.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Oracle은 가능&lt;/li&gt;
&lt;li&gt;MySQL(InnoDB)엔 비트맵 인덱스가 아예 없음&lt;/li&gt;
&lt;li&gt;PostgreSQL은 비트맵 인덱스 스캔이라는 게 있긴 한데, 사용자가 만드는 인덱스 종류가 아님 -&amp;gt; 옵티마이저가 내부적으로 쓰는 기법이라 의미가 다름&lt;/li&gt;
&lt;li&gt;사용자가 직접 만드는 비트맵 인덱스는 Oracle/DW 계열의 특징&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780131175778&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 기본: B+tree 인덱스 (키워드 없음)
CREATE INDEX idx_email ON members (email);

-- 비트맵 인덱스: BITMAP 키워드를 직접 명시
CREATE BITMAP INDEX idx_gender ON members (gender);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마지막으로 체크할 트레이드오프&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;성별 컬럼이 카디널리티 낮다고 Bitmap 붙여서 바로 만들거야?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트레이드오프 : 조회 위주야? 수정이 잦아?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;주문, 결제, 회원처럼 insert/updaterk 계속 일어나는 OLTP 테이블이면 &lt;b&gt;비트맵은 지뢰&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;한 행만 바꿔도 비트맵 한 조각에 락이 넓게 걸려서 동시 수정이 서로 막힘
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;비트들이 압축돼서 줄 단위로 통째 저장되어져 있기 때문에 이 비트 하나만 콕 집어서 수정하지 못함&lt;/li&gt;
&lt;li&gt;압축된 저장 단위 안에 들어있는 행들이 같이 묶이는 것&lt;/li&gt;
&lt;li&gt;&lt;b&gt;B+tree&lt;/b&gt;: 인덱스 항목이 &lt;b&gt;행 하나당 하나&lt;/b&gt;. 7번 수정 &amp;rarr; 7번 항목만 락. 다른 행은 자기 항목이 따로 있어서 멀쩡. 락이 &lt;b&gt;행 단위&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비트맵&lt;/b&gt;: 한 줄이 &lt;b&gt;수많은 행을 압축해 뭉쳐&lt;/b&gt; 담음. 7번 수정 &amp;rarr; 7번이 든 저장 단위를 통째로 락 &amp;rarr; 거기 같이 담긴 행들도 같이 묶임. 락이 &lt;b&gt;행보다 훨씬 넓은 단위&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;카디널리티가 낮아도 운영계 테이블엔 비트맵 X&lt;/li&gt;
&lt;li&gt;통계, 분석용처럼 밤에 한 번 적재하고 낮엔 조회만 하는 테이블(DW)이면 비트맵이 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OLTP 테이블 : Online Transaction Processing(온라인 트랜잭션 처리)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;사용자가 실시간으로 데이터를 계속 쓰고 바꾸는, 우리가 흔히 아는 운영 서비스&lt;/li&gt;
&lt;li&gt;나 같은 경우, 물류, 장바구니 서비스 내 테이블은 OLTP.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OLTP(운영계)&amp;nbsp;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;who? 실제 고객, 실시간으로&amp;nbsp;&lt;/li&gt;
&lt;li&gt;what? 주문 넣기, 장바구니 담기, 회원정보 수정, 물류 정보 insert - 읽기도 하지만 insert/update가 끊임없이 일어남&lt;/li&gt;
&lt;li&gt;특징 : 내 주문 1건 조회, 이 상품 1개 장바구니 추가 (한 번에 한두 건을 빠르게 처리)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OLAP(분석계) - DW&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;who? 기획자, 데이터팀, 분석하려고&lt;/li&gt;
&lt;li&gt;what? 지난달 30대 여성의 카테고리별 매출 합계 같은 대량 집계 조회&lt;/li&gt;
&lt;li&gt;특징 : 한 번에 수십만 ~ 수백만 건을 훑어서 합산/그룹핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;B+tree 개념이 부족해서 궁금하다면?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nu-stock.tistory.com/24&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nu-stock.tistory.com/24&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1780130760700&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;DB : B-tree, Oracle 기반으로 보자!&quot; data-og-description=&quot;B-tree디스크는 느리다.메모리에서 데이터 꺼내는 것보다 디스크(SSD)에서 꺼내는 게 수십~ 수만 배 느리다.속도 : 디스크 인덱스 목적 : 디스크를 최대한 적게 읽자!Oracle은 보통 8KB, MySQL InnoDB는 16KB&quot; data-og-host=&quot;nu-stock.tistory.com&quot; data-og-source-url=&quot;https://nu-stock.tistory.com/24&quot; data-og-url=&quot;https://nu-stock.tistory.com/24&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/drCQMB/dJMb8Yp2iAV/kviNhRivfGJgva1Mf0ak5k/img.png?width=608&amp;amp;height=520&amp;amp;face=0_0_608_520,https://scrap.kakaocdn.net/dn/8I4nI/dJMb8Xkmkrm/3Rqk6X1P1lYU3H4lVzlBW0/img.png?width=608&amp;amp;height=520&amp;amp;face=0_0_608_520,https://scrap.kakaocdn.net/dn/c6YO4E/dJMb8WMwx9L/1jjvXvU5nbQTvJ7wNI25ok/img.png?width=608&amp;amp;height=520&amp;amp;face=0_0_608_520&quot;&gt;&lt;a href=&quot;https://nu-stock.tistory.com/24&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nu-stock.tistory.com/24&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/drCQMB/dJMb8Yp2iAV/kviNhRivfGJgva1Mf0ak5k/img.png?width=608&amp;amp;height=520&amp;amp;face=0_0_608_520,https://scrap.kakaocdn.net/dn/8I4nI/dJMb8Xkmkrm/3Rqk6X1P1lYU3H4lVzlBW0/img.png?width=608&amp;amp;height=520&amp;amp;face=0_0_608_520,https://scrap.kakaocdn.net/dn/c6YO4E/dJMb8WMwx9L/1jjvXvU5nbQTvJ7wNI25ok/img.png?width=608&amp;amp;height=520&amp;amp;face=0_0_608_520');&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;DB : B-tree, Oracle 기반으로 보자!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;B-tree디스크는 느리다.메모리에서 데이터 꺼내는 것보다 디스크(SSD)에서 꺼내는 게 수십~ 수만 배 느리다.속도 : 디스크 인덱스 목적 : 디스크를 최대한 적게 읽자!Oracle은 보통 8KB, MySQL InnoDB는 16KB&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nu-stock.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>개발자</category>
      <category>면접대비</category>
      <category>빅테크가보자</category>
      <category>카디널리티</category>
      <author>누스타악</author>
      <guid isPermaLink="true">https://nu-stock.tistory.com/25</guid>
      <comments>https://nu-stock.tistory.com/25#entry25comment</comments>
      <pubDate>Sat, 30 May 2026 18:06:28 +0900</pubDate>
    </item>
    <item>
      <title>[DB] Oracle B-Tree 인덱스란? 구조와 동작 원리 정리</title>
      <link>https://nu-stock.tistory.com/24</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;B-tree&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;디스크는 느리다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;메모리에서 데이터 꺼내는 것보다 디스크(SSD)에서 꺼내는 게 수십~ 수만 배 느리다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;속도&lt;/b&gt; : 디스크 &amp;lt; 메모리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인덱스 목적&lt;/b&gt; : 디스크를 최대한 적게 읽자!&lt;/li&gt;
&lt;li&gt;Oracle은 보통 8KB, MySQL InnoDB는 16KB가 한 페이지
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;1바이트만 필요해도 페이지 통째로 읽어옴&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;노드 하나 = 페이지 하나에 키가 수백 개 들어가고, 분기가 넓어지고, 트리가 얕아짐
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&quot;넓고 얕게&quot;는 디자인 취향이 아니라 디스크 페이지 구조에서 강제된 결과&lt;/li&gt;
&lt;li&gt;데이터가 폭발적으로 늘어도 깊이는 거의 안 늘어난다(얕게 유지된다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;키가 N개면 자식 포인터는 N+1개.&lt;/li&gt;
&lt;li&gt;키 3개면 갈림길이 4개 (&amp;lt;30, 30~60, 60~90, &amp;gt;90)&lt;/li&gt;
&lt;li&gt;이 구분 키들은 길 안내용 복사본일 뿐, 실제 데이터는 맨 아래 리프에만 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKcf1a/dJMcaaFweHx/iwm5T0kmkqy1SEu99jTiQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKcf1a/dJMcaaFweHx/iwm5T0kmkqy1SEu99jTiQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKcf1a/dJMcaaFweHx/iwm5T0kmkqy1SEu99jTiQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKcf1a%2FdJMcaaFweHx%2Fiwm5T0kmkqy1SEu99jTiQk%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;662&quot; height=&quot;270&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;B+tree의 개념과 B-tree의 개념 차이
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;실제 데이터가 어디에 있느냐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lCNVQ/dJMb990T4N2/EVp2VBislTeKXqM2qcBGM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lCNVQ/dJMb990T4N2/EVp2VBislTeKXqM2qcBGM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lCNVQ/dJMb990T4N2/EVp2VBislTeKXqM2qcBGM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlCNVQ%2FdJMb990T4N2%2FEVp2VBislTeKXqM2qcBGM1%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;520&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Oracle은 B+tree를 쓰고 있고 대부분의 DB(Orcale, MySQL, InnoDB 등)가 B+tree를 쓰고 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이유 : 트리가 더 얕아짐 -&amp;gt; 결론 디스크 적게 읽기&lt;/li&gt;
&lt;li&gt;내부 노드에 데이터를 안 실으니 한 페이지에 키를 더 많이 욱여넣을 수 있음 -&amp;gt; 분기가 더 넓어지고 -&amp;gt; 트리가 더 얕아지고 -&amp;gt; 디스크를 더 적게 읽음.&lt;/li&gt;
&lt;li&gt;범위 검색이 저렴
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;WHERE 가격 BETWEEN 50 AND 80, ORDER BY, 페이지네이션 - 이런 게 전부 빨라짐.&lt;/li&gt;
&lt;li&gt;시작점 리프를 찾은 다음 연결된 리프를 옆으로 쭉 훑으면 끝.&lt;/li&gt;
&lt;li&gt;위로 다시 올라갔다 내려올 필요가 없음. (그냥 B-tree는 범위 검색할 때 트리를 계속 오르내려야 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;조회 비용이 일정
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;B+tree는 &lt;b&gt;항상 리프(맨 아래)까지&lt;/b&gt; 가니까 어떤 값이든 비용이 똑같&lt;/li&gt;
&lt;li&gt;B-tree : 쿼리마다 속도가 들쭉날쭉&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;키가 내부 노드(이정표)랑 리프(실제 데이터) &lt;b&gt;양쪽에 중복 저장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;B+tree - 숫자 좀 두 번 적는 약간의 공간 낭비&quot;를 감수하는 대신, 트리는 더 얕아지고(디스크 &amp;darr;) 범위 검색은 빨라짐&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  개발 성장 로그</category>
      <author>누스타악</author>
      <guid isPermaLink="true">https://nu-stock.tistory.com/24</guid>
      <comments>https://nu-stock.tistory.com/24#entry24comment</comments>
      <pubDate>Sat, 30 May 2026 17:03:10 +0900</pubDate>
    </item>
  </channel>
</rss>