<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>매일 매일 꾸준히</title>
    <link>https://loan-project.tistory.com/</link>
    <description>기술 탐구 , 프로젝트 진행 포스팅</description>
    <language>ko</language>
    <pubDate>Sat, 18 Jul 2026 07:37:22 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>watanabe3</managingEditor>
    <image>
      <title>매일 매일 꾸준히</title>
      <url>https://tistory1.daumcdn.net/tistory/8520054/attach/67d3ba8acda847b9b602dbf815ef9cbc</url>
      <link>https://loan-project.tistory.com</link>
    </image>
    <item>
      <title>은행 아키텍처 알아보기</title>
      <link>https://loan-project.tistory.com/31</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며 (동기)&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출금 상환 프로젝트는 실제 은행 시스템을 모방하여 설계하고 이 과정에서 신뢰성 , 성능 등의 문제 상황을 가정 , 이를 해결해보는 개인 포트폴리오 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처를 설계하기 전 , 아키텍처 스타일에는 어떤 것들이 있으며 , 실제 은행 아키텍처는 어떤지 공부해야할 필요성을 느꼈습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 글에서 , 이를 공부하고 글로 남겨보겠습니다. (검수 필요)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;아키텍처 스타일 : 모놀리식 vs MSA&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 어떻게 나누고 배포할 것인가는 아키텍처를 설계할 때 가장 먼저 마주하는 질문 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 비교되는 2가지 스타일인 모놀리식과 MSA 를 정리해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;모놀리식 아키텍처&amp;nbsp;&lt;/b&gt;&lt;/h3&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;1024&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYvYqy/dJMcaf1iPJw/KkBa5c7t5PudkVZIdY5kSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYvYqy/dJMcaf1iPJw/KkBa5c7t5PudkVZIdY5kSk/img.png&quot; data-alt=&quot;모놀리식 은행 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYvYqy/dJMcaf1iPJw/KkBa5c7t5PudkVZIdY5kSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYvYqy%2FdJMcaf1iPJw%2FKkBa5c7t5PudkVZIdY5kSk%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;625&quot; height=&quot;393&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;644&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모놀리식 은행 아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식은 서비스의 기능들이 하나의 애플리케이션 안에 모두 포함되고 , 하나의 데이터베이스를 공유하는 구조 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 은행 시스템이라면, 수신&amp;middot;여신&amp;middot;카드&amp;middot;회계 기능이 단일 서버 안에 함께 구현되고 하나의 DB를 바라보게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적이고 직관적인 구성 방식이라 처음 아키텍처를 설계할 때 가장 많이 선택되는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조가 단순해 이해하고 개발하기 쉽습니다.&lt;/li&gt;
&lt;li&gt;하나의 코드베이스, 하나의 배포 파이프라인이라 빠르게 개발하고 배포할 수 있습니다.&lt;/li&gt;
&lt;/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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 규모가 커질수록 코드 복잡도가 증가하고 유지보수가 어려워집니다.&lt;/li&gt;
&lt;li&gt;일부 기능만 트래픽이 몰려도 전체를 함께 확장해야 해서 확장성과 가용성이 떨어집니다.&lt;/li&gt;
&lt;li&gt;성능 최적화나 부분적인 분산 처리에 한계가 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MSA (마이크로 서비스 아키텍처)&lt;/b&gt;&lt;/h3&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;1024&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/erBYtE/dJMcajimzz6/V1C2pKmqjBrBxKTaAtK160/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/erBYtE/dJMcajimzz6/V1C2pKmqjBrBxKTaAtK160/img.png&quot; data-alt=&quot;MSA 은행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/erBYtE/dJMcajimzz6/V1C2pKmqjBrBxKTaAtK160/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FerBYtE%2FdJMcajimzz6%2FV1C2pKmqjBrBxKTaAtK160%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;752&quot; height=&quot;438&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MSA 은행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA는 동일한 은행 도메인을 여신, 회계, 수신, 카드처럼 기능 단위로 완전히 쪼개서, 각각을 독립된 애플리케이션으로 만들고 독립된 DB를 갖게 하는 구조입니다. 모놀리식이 하나의 애플리케이션&amp;middot;하나의 DB였다면, MSA는 도메인 경계를 따라 애플리케이션과 DB를 모두 나눈다는 점이 가장 핵심적인 차이입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 물리적으로 분리되어 있으니, 서비스 간에 필요한 정보를 주고받으려면 더 이상 메서드 호출이 아니라 네트워크를 통한 통신이 필요해집니다. 이때 즉시 응답이 필요한 경우에는 REST API 같은 동기 방식을, 응답을 바로 기다릴 필요가 없는 경우에는 메시지 큐를 통한 비동기 방식을 사용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;서비스마다 다른 언어, 프레임워크, DB를 자유롭게 선택할 수 있습니다.&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;여러 DB에 걸친 데이터 일관성 관리, 서비스 간 통신 장애 처리 등 설계 난이도가 올라갑니다.&amp;nbsp;&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;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;은행 아키텍처 들여다보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEJvKd/dJMb99Nxadp/icybOtf6qGIr3vWNXxSB70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEJvKd/dJMb99Nxadp/icybOtf6qGIr3vWNXxSB70/img.png&quot; data-alt=&quot;출처 : https://toss.tech/article/slash23-corebanking&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEJvKd/dJMb99Nxadp/icybOtf6qGIr3vWNXxSB70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEJvKd%2FdJMb99Nxadp%2FicybOtf6qGIr3vWNXxSB70%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;1538&quot; height=&quot;780&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://toss.tech/article/slash23-corebanking&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;채널&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널이란 고객이 은행 서비스에 접근하는 모든 접점(인터페이스)을 의미합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;어떤 수단으로 은행과 상호작용하는가&quot;를 가리키는 개념입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널은 크게 두 갈래로 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대면 채널 : 직원이 직접 응대하는 방식 (영업점 창구 , 텔러 단말)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비대면 채널 : 직원 개입 없이 고객이 직접 접근 하는 방식 (ATM , 인터넷 뱅킹 , 뱅킹앱 , 오픈 뱅킹 API)&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;채널계&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널들이 보낸 요청은 곧바로 코어뱅킹으로 가지 않습니다. 그 사이에서 다양한 채널의 요청을 표준화해 코어뱅킹으로 전달하는 시스템이 채널계입니다. 보통 채널계는 자체 DB 를 두고, MCI(Multi Channel Integration)라는 미들웨어를 통해 코어뱅킹과 통신합니다. 단순히 &quot;요청을 모아서 전달&quot;하는 수준을 넘어, 실질적으로 다음과 같은 책임을 함께 지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전문 변환과 라우팅&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뱅킹앱이 보내는 요청과 ATM 이 보내는 요청의 포맷은 다릅니다. 채널계는 이 서로 다른 포맷을 코어뱅킹이 이해하는 표준 전문 형식으로 변환하고, 어떤 업무(수신,여신,카드)로 보낼지 라우팅 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인증과 세션 관리&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객이 앱에 로그인하면, 그 세션이 유효한지&amp;middot;토큰이 만료되지 않았는지 같은 인증 검증을 채널계가 1차로 처리합니다. 코어뱅킹이 매 요청마다 인증을 다시 확인하지 않도록, 채널계가 일종의 게이트 역할을 하는 셈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;화면용 데이터 조합 (Backend For Frontend) &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면을 그리는 일 자체는 모바일 앱이나 웹 클라이언트가 담당합니다. 다만 화면 하나를 채우는 데 필요한 데이터가 여러 코어뱅킹 거래에 걸쳐 있는 경우가 많아서, 채널계가 이 거래들을 대신 호출하고 화면에 맞는 형태로 조합해 클라이언트에 내려주는 역할을 합니다. 반면 ATM이나 영업점 단말처럼 클라이언트가 가벼운 채널에서는, 어떤 화면을 다음에 보여줄지 같은 흐름 제어까지 채널계가 더 깊이 관여하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로깅&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널계에는 주로 웹로그(접속 이력, 화면 흐름 등)가 쌓이고, 코어뱅킹에는 업무 처리 로그&amp;middot;거래 내역이 쌓입니다. 두 로그는 성격이 달라서 별도로 적재되고, 이후 EAI 같은 미들웨어를 거쳐 분석 시스템으로 모입니다.&lt;/p&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;middot;성능 병목 관리&lt;/b&gt;&lt;br /&gt;구조적으로 모든 채널의 트래픽이 한 곳(MCI)으로 몰리기 때문에, MCI에 장애가 생기면 시스템 전체 장애로 번질 수 있어 별도의 장애 대응 방안이 필요합니다. 그래서 채널계는 단일 장애점(SPOF)이 되지 않도록 이중화나 부하분산 설계가 중요한 영역 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코어뱅킹 (계정계)&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널계가 전달한 요청을 받아 실제로 처리하는 곳이 코어뱅킹입니다. 금원과 관련된 메인 비즈니스 로직을 처리하는 시스템으로, 계정계라고도 부릅니다. 여신, 수신, 외국환 업무 등 고객과의 직접적인 금융거래를 담당하며, 금융회사 전체의 정보시스템을 통제하는 핵심 솔루션입니다. 계좌 잔액, 거래 원장 같은 가장 민감하고 중요한 데이터가 여기서 관리됩니다. 한 번 구축되면 최소 5~7년간 사용되고, 구축 비용도 수백억 원에 달할 만큼 무겁고 보수적으로 다뤄지는 시스템입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모놀리식 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코어뱅킹은 대부분의 은행에서 여전히 거대한 모놀리식 구조로 운영되고 있습니다. 채널이 늘어날 때마다(폰뱅킹 &amp;rarr; 인터넷뱅킹 &amp;rarr; 모바일뱅킹) 기존 시스템에 기능을 계속 얹어온 결과로, 처음부터 모놀리식을 의도했다기보다는 자연스럽게 굳어진 형태에 가깝습니다. 여신&amp;middot;수신&amp;middot;카드&amp;middot;회계가 한 코드베이스에 있을 뿐 아니라, 채널계와의 통신을 위한 MCI, 대외연계를 위한 FEP, 대내 시스템과의 연계를 위한 EAI까지 코어뱅킹 서버에 강결합되어 있는 구조입니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;코어뱅킹의 MSA 전환 사례 - 토스뱅크&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트/대출금 상환 프로젝트</category>
      <author>watanabe3</author>
      <guid isPermaLink="true">https://loan-project.tistory.com/31</guid>
      <comments>https://loan-project.tistory.com/31#entry31comment</comments>
      <pubDate>Sun, 21 Jun 2026 17:15:27 +0900</pubDate>
    </item>
    <item>
      <title>[6] 데이터베이스 물리적 모델링</title>
      <link>https://loan-project.tistory.com/30</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 대출금 상환 프로젝트에 대해 개념적 , 논리적 데이터베이스 모델링을 진행했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 , 대출계약 , 대출계좌 , 상환 스케줄 , 연체 이력 등 15 개 테이블과 그 관계를 정의하고 정규화 과정 까지 마쳤습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 그 결과 ERD 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;1306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfbBpd/dJMcaiXUCkj/IRATYKdOwRhq4Yh6yNUi2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfbBpd/dJMcaiXUCkj/IRATYKdOwRhq4Yh6yNUi2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfbBpd/dJMcaiXUCkj/IRATYKdOwRhq4Yh6yNUi2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfbBpd%2FdJMcaiXUCkj%2FIRATYKdOwRhq4Yh6yNUi2K%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;1258&quot; height=&quot;1306&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;1306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 논리적 모델링 결과를 바탕으로 물리적 모델링을 진행하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리적 모델링은 ERD 를 바탕으로 MySQL 이라는 구체적인 DBMS 에 최적화된 형태로 구현하는 과정 입니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;진행 순서&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리적 모델링은 다음 3단계로 진행 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테이블과 컬럼 변환&lt;/b&gt;: 논리적 모델의 한글 엔티티명/속성명을 명명 규칙(Naming Convention)에 따라 영문으로 변환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 타입 정의&lt;/b&gt;: 각 컬럼에 가장 적합한 자료형(VARCHAR, BIGINT, DECIMAL 등)을 선택합니다. 저장 공간과 성능에 직접적인 영향을 미치는 작업입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제약조건 설정&lt;/b&gt;: 기본 키, 외래 키, NOT NULL, UNIQUE 등 데이터 무결성을 보장하기 위한 제약조건을 구체적으로 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;물리적 모델링 결과 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리적 모델링을 마치면 2가지 산출물이 나옵니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테이블 정의서&lt;/b&gt;: 각 테이블과 컬럼이 무엇을 의미하고 어떤 규칙을 가지는지 상세히 기술한 설계 명세서입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DDL 스크립트&lt;/b&gt;: 실제로 CREATE TABLE을 실행해서 테이블을 생성할 수 있는 SQL 스크립트입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;코드성 테이블&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드성 테이블(이자종류, 상환방법, 대출상태, 상환스케줄상태, 연체이력상태 , 연체근거)은 모두 동일한 패턴을 가집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNtjPK/dJMcaip8rny/dkhhi3IrLz9VffGu5RgPKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNtjPK/dJMcaip8rny/dkhhi3IrLz9VffGu5RgPKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNtjPK/dJMcaip8rny/dkhhi3IrLz9VffGu5RgPKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNtjPK%2FdJMcaip8rny%2Fdkhhi3IrLz9VffGu5RgPKk%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;1014&quot; height=&quot;286&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PK 가 INT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 수가 명확히 제한된 코드성 데이터라 BIGINT 가 불필요 하다고 생각했습니다.&lt;/p&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;컬럼명이 name&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블명이 이미 컨텍스트를 주므로 (interest_type 안의 name 은 당연히 이자종류명) interest_type_name 처럼 접두사를 붙이지 않습니다.&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;1. 이자종류 (interest_type)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고정금리, 변동금리와 같은 대출계약에 적용되는 이자 산정 방식의 종류를 관리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 상환방법 (repayment_method)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원리금균등상환, 원금균등상환, 만기일시상환와 같은 대출상품이 지원하는 상환 방식을 관리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 대출상태 (loan_status)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출계좌의 현재 상태를 관리합니다. 대출상태 이력을 통해 시점별 변화를 추적합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 상환스케줄상태 (repayment_schedule_status)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상환 스케줄 한 회차가 현재 어떤 상태인지를 관리합니다. 상환 스케줄이 참조합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 연체이력상태 (delinquency_history_status)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연체 이력 한 건이 현재 진행 중인지, 해소되었는지 등을 관리합니다. 연체 이력이 참조합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;6. 연체근거 (delinquency_reason)&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;연체 이력 한 건에 대해 연체 근거를 관리합니다. 연체 이력이 참조합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;핵심 테이블&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 회원 (member)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ck0eeN/dJMcajigTOd/YnLBrZtwgqx5u492hcC7R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ck0eeN/dJMcajigTOd/YnLBrZtwgqx5u492hcC7R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ck0eeN/dJMcajigTOd/YnLBrZtwgqx5u492hcC7R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fck0eeN%2FdJMcajigTOd%2FYnLBrZtwgqx5u492hcC7R0%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;1016&quot; height=&quot;396&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;member_id BIGINT &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원은 계속 누적되는 핵심 데이터입니다. 서비스를 지속하면서 계속 늘어나는 데이터 이므로 BIGINT 를 사용하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가입일시 &amp;rarr; created_at&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD의 가입일시는 &quot;회원이 가입한 시점&quot;이라는 비즈니스 이벤트입니다. 명명 규칙상 이벤트 발생 시점은 _at 접미사를 쓰고, 이는 곧 &quot;데이터가 처음 생성된 시점&quot;과 동일하므로 별도 컬럼 없이 공통 컬럼 created_at으로 흡수합니다.&lt;/p&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;password VARCHAR(255)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평문이 아닌 해시값(BCrypt 등)이 저장되므로, 원본 비밀번호 길이보다 해시 결과 길이를 고려해 충분히 여유 있게 잡았습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 주 계좌 (primary_account)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWavnJ/dJMcaaFHrSH/5Jy2lNsNWv5dxw3Uh8UnI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWavnJ/dJMcaaFHrSH/5Jy2lNsNWv5dxw3Uh8UnI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWavnJ/dJMcaaFHrSH/5Jy2lNsNWv5dxw3Uh8UnI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWavnJ%2FdJMcaaFHrSH%2F5Jy2lNsNWv5dxw3Uh8UnI0%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;1014&quot; height=&quot;490&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;member_id에 UQ&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD상 회원:주계좌가 1:1 관계 입니다. FK에 UNIQUE를 걸어 &quot;한 회원은 주 계좌를 하나를 반드시 가진다&quot;는 제약을 DB 레벨에서 강제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;계좌번호 &amp;rarr; account_no&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보편적인 약어 활용을 하여 number &amp;rarr; no로 축약했습니다.&lt;/p&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;rarr; DECIMAL(23,4)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 데이터는 부동소수점(FLOAT/DOUBLE) 사용 시 미세한 오차가 발생할 수 있어 절대 사용하지 않습니다. DECIMAL(23,4)로 정수부 19자리, 소수부 4자리까지 정확하게 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;balance DEFAULT 0&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계좌 생성 시점에 잔액이 0원인 경우가 일반적이므로 기본값을 명시했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. 대출상품 (loan_product)&lt;/h3&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;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w4Pfd/dJMcabExgdR/AlUa8xzQagcKPqkH8pFDG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w4Pfd/dJMcabExgdR/AlUa8xzQagcKPqkH8pFDG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w4Pfd/dJMcabExgdR/AlUa8xzQagcKPqkH8pFDG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw4Pfd%2FdJMcabExgdR%2FAlUa8xzQagcKPqkH8pFDG0%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;1028&quot; height=&quot;516&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FK interest_type_id, repayment_method_id 타입 일치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FK는 참조하는 PK와 데이터 타입이 반드시 같아야 합니다. 이자종류, 상환방법의 PK가 INT이므로 여기서도 INT를 사용합니다.&lt;/p&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;rarr; DECIMAL(10,5)&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기준금리 + 가산금리 - 우대금리 구조에서 가산금리는 2.50000%처럼 소수점 5자리까지의 정밀도가 필요할 수 있습니다. DECIMAL(10,5)는 정수부 5자리, 소수부 5자리까지 &amp;mdash; 최대 99999.99999까지 표현 가능해 여유 있게 설정했습니다. 이후 등장할 모든 금리(적용금리, 연체이자율 등) 컬럼에도 동일한 DECIMAL(10,5)를 적용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;운영 테이블&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 대출계약 (loan_contract)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctrT1l/dJMcahkwC0F/ylxKOac05VH44DKcraR29K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctrT1l/dJMcahkwC0F/ylxKOac05VH44DKcraR29K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctrT1l/dJMcahkwC0F/ylxKOac05VH44DKcraR29K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctrT1l%2FdJMcahkwC0F%2FylxKOac05VH44DKcraR29K%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;1020&quot; height=&quot;574&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;계약일/만기일 &amp;rarr; _date&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시각 정보가 의미 없는 날짜 전용 데이터입니다. 명명 규칙상 _date 접미사를 사용합니다.&lt;/p&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;rarr; contract_principal&lt;/b&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;ERD에서 대출계좌의 대출원금잔액(현재 남은 금액, 변동값)과 구분하기 위해 _principal로만 명명하고 balance를 붙이지 않았습니다.&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;FK member_id에는 UQ 없음&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주계좌와 달리 대출계약은 ERD상 회원:계약이 1:N입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 회원이 여러 계약을 가질 수 있으므로 UNIQUE 제약을 걸지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 대출계좌 (loan_account)&lt;/h3&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;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLtQCn/dJMcabExgoS/D0HksueMUxUIkWWlSvGR21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLtQCn/dJMcabExgoS/D0HksueMUxUIkWWlSvGR21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLtQCn/dJMcabExgoS/D0HksueMUxUIkWWlSvGR21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLtQCn%2FdJMcabExgoS%2FD0HksueMUxUIkWWlSvGR21%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;1028&quot; height=&quot;666&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;loan_contract_id에 UQ&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD상 대출계약:대출계좌가 1:1 관계입니다. &quot;한 계약은 하나의 대출계좌만 가진다&quot;를 DB 레벨에서 강제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대출원금잔액 &amp;rarr; loan_principal_balance&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출계약.contract_principal(계약 시점의 고정 원금)과 달리, 상환이 진행되면서 계속 줄어드는 현재 잔액입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 _balance 접미사를 붙여 변동값임을 명확히 했습니다.&lt;/p&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;rarr; monthly_payment_day TINYINT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매달 1~31일 중 한 값만 가지므로 TINYINT(&amp;minus;128~127)로 충분합니다. INT를 쓰면 4바이트가 낭비됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 회차 &amp;rarr; current_installment_no INT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보편적인 약어 활용에 따라 number &amp;rarr; no로 축약했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회차는 보통 12회, 36회 등 INT 범위 내에서 충분합니다.&lt;/p&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;FK primary_account_id 타입&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주계좌의 PK가 BIGINT이므로 동일하게 BIGINT입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. 대출상태 이력 (loan_status_history)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1pUpr/dJMcaaeBxOb/MNnzc9B4eOYhnD4wSFNeuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1pUpr/dJMcaaeBxOb/MNnzc9B4eOYhnD4wSFNeuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1pUpr/dJMcaaeBxOb/MNnzc9B4eOYhnD4wSFNeuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1pUpr%2FdJMcaaeBxOb%2FMNnzc9B4eOYhnD4wSFNeuk%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;1024&quot; height=&quot;518&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PK가 BIGINT &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;처음엔 적어 보여도 장기적으로 가장 빠르게 row 수가 늘어날 수 있어 BIGINT를 채택했습니다.&lt;/p&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;rarr; end_date NULL 허용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 진행 중인 상태는 종료일이 정해지지 않았습니다. &quot;현재 상태&quot;는 end_date IS NULL인 행으로 식별합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;loan_account_id에 UQ 없음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 대출계좌는 여러 개의 상태 이력(정상&amp;rarr;연체&amp;rarr;정상...)을 가질 수 있으므로(1:N) UNIQUE를 걸지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;상환 테이블&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 상환 스케줄 (repayment_schedule)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ywvK5/dJMcadCf8t0/ZftGL8tuaT91tyvfMOyur1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ywvK5/dJMcadCf8t0/ZftGL8tuaT91tyvfMOyur1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ywvK5/dJMcadCf8t0/ZftGL8tuaT91tyvfMOyur1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FywvK5%2FdJMcadCf8t0%2FZftGL8tuaT91tyvfMOyur1%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;1020&quot; height=&quot;798&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예정일 &amp;rarr; scheduled_at&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출계좌 생성 시점에 1회차~N회차 일정이 한 번에 확정되고 이후 변경되지 않는 값이므로, &quot;변경 가능&quot;이라는 의미를 가진 _dt는 맞지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 _at은 &quot;특정 작업이 발생한 정확한 시점&quot;을 의미하며, &quot;1회차 상환은 2026-07-15에 발생하기로 확정되었다&quot;는 확정된 사건의 시점으로 해석할 수 있어 _at을 사용했습니다.&lt;/p&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;rarr; 컬럼 분리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD에는 상환금 (원금,이자) 하나로 표기되어 있지만, 물리 모델에서는 scheduled_principal, scheduled_interest 두 컬럼으로 분리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원금과 이자는 회계적으로 성격이 다른 값이고, 변제충당순서(이자 우선 vs 원금 우선) 로직에서 각각 독립적으로 다뤄야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합산값 하나만 저장하면 상환이력에서 &quot;원금 얼마/이자 얼마 납부했는지&quot;를 계산하기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용금리 &amp;rarr; applied_rate DECIMAL(10,5)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이자율은 4.50000%처럼 소수점 5자리까지의 정밀도가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DECIMAL(10,5)는 정수부 5자리, 소수부 5자리까지 표현 가능해 충분한 여유를 둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 상환 이력 (repayment_history)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c49Vow/dJMcad3nQnC/50QKzgKfxHRsVj0jCnutP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c49Vow/dJMcad3nQnC/50QKzgKfxHRsVj0jCnutP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c49Vow/dJMcad3nQnC/50QKzgKfxHRsVj0jCnutP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc49Vow%2FdJMcad3nQnC%2F50QKzgKfxHRsVj0jCnutP0%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;1024&quot; height=&quot;772&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;납부 일시 &amp;rarr; paid_at&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;회원이 납부했다&quot;는 이미 발생한 비즈니스 이벤트이므로 _at을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;created_at(이 row가 DB에 생성된 시점)과는 의미가 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치가 다음날 새벽에 상환이력을 생성한다면, paid_at은 실제 납부 시각, created_at은 배치가 row를 만든 시각으로 구분됩니다.&lt;/p&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;delinquency_history_id &amp;rarr; NULL 허용&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD상 상환이력:연체이력은 선택적 관계 입니다. 정상 상환은 연체와 무관하므로 FK 값이 없을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;납부 연체금 &amp;rarr; paid_delinquency_charge DEFAULT 0&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연체 없는 정상 상환에서는 0원이 들어가는 것이 자연스러우므로 기본값을 0으로 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. 연체 이력 (delinquency_history)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmfz1p/dJMcahEJiRt/68eKYto1pv9KsvK3jyXVJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmfz1p/dJMcahEJiRt/68eKYto1pv9KsvK3jyXVJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmfz1p/dJMcahEJiRt/68eKYto1pv9KsvK3jyXVJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcmfz1p%2FdJMcahEJiRt%2F68eKYto1pv9KsvK3jyXVJ1%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;1018&quot; height=&quot;670&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연체 일시 &amp;rarr; delinquent_at&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;연체가 발생했다&quot;는 이미 일어난 이벤트이므로 _at을 사용합니다.&lt;/p&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;근거id &amp;rarr; delinquency_reason_id, NOT NULL&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연체 이력이 생성되었다면 그 발생 사유는 반드시 존재하는 정보이므로, 코드성 테이블 delinquency_reason을 참조하는 FK를 NOT NULL로 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연체 이자율 &amp;rarr; delinquent_rate DECIMAL(10,5)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상환 스케줄.applied_rate와 동일한 정밀도가 필요합니다. 연체이자율은 보통 정상 적용금리 + 가산율로 계산되기 때문입니다.&lt;/p&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;repayment_schedule_id에 UQ 없음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD상 상환스케줄:연체이력은 1:N 입니다. 한 회차가 여러 일에 걸쳐 연체를 할 수 있으므로 UNIQUE를 걸지 않았습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리 &amp;mdash; 테이블 정의서와 DDL&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리적 모델링에서 정의한 15개 엔티티를 명명 규칙에 따라 영문 테이블/컬럼으로 변환하고, 데이터 타입과 제약조건까지 결정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과물로 두 가지를 정리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 테이블 정의서&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 정의서는 DDL만으로는 알 수 없는 정보를 담는 문서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[링크]&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. DDL 스크립트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 CREATE TABLE을 실행해서 테이블을 생성할 수 있는 SQL 스크립트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[링크]&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트/대출금 상환 프로젝트</category>
      <author>watanabe3</author>
      <guid isPermaLink="true">https://loan-project.tistory.com/30</guid>
      <comments>https://loan-project.tistory.com/30#entry30comment</comments>
      <pubDate>Sun, 14 Jun 2026 14:13:58 +0900</pubDate>
    </item>
    <item>
      <title>[5] 데이터베이스 논리적 모델링2 - 정규화</title>
      <link>https://loan-project.tistory.com/29</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스를 설계할 때 테이블을 어떻게 나눌지 고민한 경험이 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감각적으로 &quot;이건 따로 빼야 할 것 같은데&quot;라는 판단을 한 적도 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규화는 그 판단을 이론적으로 뒷받침해주는 도구입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정규화란 무엇인가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규화(Normalization)는 데이터의 중복을 최소화하고 일관성을 보장하기 위해, 테이블을 정규형(Normal Form)이라는 규칙에 맞게 변환하는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 개념은 **함수 종속성(Functional Dependency)**입니다. X &amp;rarr; Y로 표기하며, &quot;X 값이 Y 값을 유일하게 결정한다&quot;는 의미입니다. 정규화는 이 종속 관계를 분석해서 잘못된 구조를 찾아내고 테이블을 올바르게 분리하는 작업입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 필요한가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규화를 적용하지 않으면 세 가지 이상 현상(Anomaly)이 발생합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;갱신 이상&lt;/b&gt;: 동일한 데이터가 여러 행에 중복 저장되어 있을 때, 하나만 수정하면 데이터 불일치가 생깁니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;삽입 이상&lt;/b&gt;: 특정 데이터가 없으면 다른 데이터도 저장할 수 없는 구조가 만들어집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;삭제 이상&lt;/b&gt;: 하나의 행을 삭제했을 때 의도치 않게 다른 정보까지 사라집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 단계별로 정규형을 적용합니다. 실무에서는 보통 3NF 또는 BCNF까지 적용하는 것을 목표로 합니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이 글의 목적&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 제가 설계한 &lt;b&gt;대출금 상환 테이블&lt;/b&gt;을&amp;nbsp;대상으로 직접 정규화를 적용해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론만 아는 것과, 실제 설계에 대입해 &quot;이 테이블은 왜 이렇게 나눴는가&quot;를 설명할 수 있는 것은 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 정규형 단계별로 설계가 조건을 만족하는지 체크하고 그 근거를 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;대상 테이블 요약&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;1026&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HiMt1/dJMcadvqEVu/Pna2QAZgFSKxthmM8gTcFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HiMt1/dJMcadvqEVu/Pna2QAZgFSKxthmM8gTcFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HiMt1/dJMcadvqEVu/Pna2QAZgFSKxthmM8gTcFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHiMt1%2FdJMcadvqEVu%2FPna2QAZgFSKxthmM8gTcFK%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;1238&quot; height=&quot;1026&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;1026&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;제1 정규형(1NF) 체크&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1NF 란&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1NF 의 조건은 테이블의 모든 컬럼잉 원자적인 값을 가져야 한다는 것 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원자적이라는 것은 더 이상 쪼갤 수 없는 단일 값을 의미합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 컬럼에 여러 값을 쉼표로 구분해 넣거나, 중첩된 구조로 저장하는 경우가 대표적인 위반 사례 입니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;체크 결과&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모든 컬럼이 단일 값을 가집니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상환스케줄의 상환금(원금,이자)는 표기상 하나의 컬럼처럼 보이지만 , 실제 테이블에서는 원금과 이자를 별도 컬럼으로 분리합니다. 하나의 컬럼에 두 값을 묶어 저장하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반복 그룹이 없습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출계약 한 건에 상환 스케줄이 여러 개 연결될 수 있지만 , 이를 대출계약 테이블 내에 배열처럼 저장하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상환 스케줄을 별도 테이블로 분리하고 FK 로 참조하는 구조입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드성 값을 별도 테이블로 분리합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태명을 문자열로 직접 컬럼에 저장하게 되면, 이는 경우에 따라 원자성 위반으로 이어질 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이자종류, 상환방법, 대출상태, 상환상태, 연체상태를 각각 독립적인 테이블로 분리하고 이를 FK 로 참조하는 구조를 유지하여 이를 예방합니다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;결론&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;14개 테이블 모두 1NF를 만족합니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;제2 정규형(2NF) 체크&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2NF 란&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2NF의 조건은 두 가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1NF를 만족해야 하고, 모든 컬럼이 기본 키 전체에 대해 &lt;b&gt;완전 함수 종속&lt;/b&gt;이어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 복합 PK의 일부에만 종속되는 컬럼이 있으면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;b&gt;부분 함수 종속&lt;/b&gt;이라고 하며, 2NF는 이를 제거하는 과정입니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;체크 결과&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부분 함수 종속은 복합 PK가 존재할 때만 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 14개 테이블을 확인한 결과, 모든 테이블이 자연키 대신 단일 대리키를 PK로 사용하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복합 PK가 없으므로 부분 함수 종속이 발생할 수 없는 구조입니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;결론&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;14개 테이블 모두 2NF를 만족합니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;제3 정규형 (3NF) 체크&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3NF란&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3NF의 조건은 두 가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2NF를 만족해야 하고, &lt;b&gt;이행적 함수 종속&lt;/b&gt;이 없어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이행적 함수 종속이란 PK &amp;rarr; 일반 컬럼A &amp;rarr; 일반 컬럼B 형태로, 기본 키가 아닌 일반 컬럼이 다른 일반 컬럼을 결정하는 관계를 의미합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;체크 결과&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이행적 종속이 발생할 수 있는 후보를 중심으로 확인했습니다.&lt;/p&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;스케줄id &amp;rarr; 대출계좌id &amp;rarr; 계약id &amp;rarr; 상품id &amp;rarr; 가산금리 같은 체인이 성립한다면 이행적 종속으로 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 적용금리는 대출상품의 가산금리를 그대로 참조하는 것이 아니라, 스케줄 생성 시점에 확정된 금리를 스냅샷으로 저장한 값입니다. 즉, 스케줄id &amp;rarr; 적용금리로 직접 종속되는 독립적인 컬럼이므로 이행적 종속이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상환이력의 연체id FK&lt;/b&gt; 도 검토 대상입니다. 연체가 발생한 경우에만 연체이력을 참조하고, 연체가 없으면 NULL을 허용하는 구조입니다. 연체이력의 속성들이 상환이력 내에 중복 저장되지 않고 FK 참조로만 연결되어 있으므로 이행적 종속이 발생하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외 나머지 테이블들도 일반 컬럼이 다른 일반 컬럼을 결정하는 관계가 없습니다. FK 컬럼은 모두 참조 역할만 하며, 각 테이블의 컬럼들은 PK에만 직접 종속됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;결론&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;14개 테이블 모두 3NF를 만족합니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;BCNF 체크&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BCNF란&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BCNF는 3NF를 강화한 버전으로, 모든 결정자가 후보 키 여야 한다는 조건입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후보 키란 행을 유일하게 식별할 수 있는 컬럼으로, PK뿐 아니라 UQ 제약이 걸린 컬럼도 포함됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3NF를 만족하더라도 후보 키가 아닌 일반 컬럼이 다른 컬럼을 결정하는 관계가 있다면 BCNF를 위반합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;체크 결과&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BCNF 위반 여부는 UQ 컬럼이 다른 일반 컬럼을 결정하는 관계가 있는지를 중심으로 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UQ 제약이 걸린 컬럼은 회원의 로그인id, 주계좌의 계좌번호, 대출계좌의 계좌번호, 코드성 테이블들의 상태명/방법명입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컬럼들은 모두 행을 유일하게 식별하는 후보 키 역할을 하며, 다른 일반 컬럼을 결정하는 관계가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 모든 결정자가 후보 키이므로 BCNF 조건을 만족합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;결론&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;14개 테이블 모두 BCNF를 만족합니다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1NF부터 BCNF까지 전체 14개 테이블을 체크한 결과, 모든 테이블이 각 정규형의 조건을 만족했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 단일 대리키를 PK로 사용하고, 코드성 데이터를 별도 테이블로 분리하며, 반복 그룹 없이 FK 참조 구조를 일관되게 적용한 것이 정규화 조건을 자연스럽게 충족한 배경입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 논리적으로 올바른 이 설계를 MySQL에 최적화된 형태로 구현하는 &lt;b&gt;물리적 모델링&lt;/b&gt;을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 타입 선정, 인덱스 설계 , 제약 조건 설정 등을 다룰 것 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트/대출금 상환 프로젝트</category>
      <author>watanabe3</author>
      <guid isPermaLink="true">https://loan-project.tistory.com/29</guid>
      <comments>https://loan-project.tistory.com/29#entry29comment</comments>
      <pubDate>Sat, 6 Jun 2026 16:11:38 +0900</pubDate>
    </item>
    <item>
      <title>[4] 데이터베이스 논리적 모델링1 - 테이블 설계</title>
      <link>https://loan-project.tistory.com/28</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 대출 도메인의 비즈니스 요구사항을 바탕으로 개념적 모델링을 완성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 논리적 모델링 1편으로, 개념적 모델의 엔티티를 관계형 데이터베이스의 테이블로 변환하는 과정을 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리적 모델링 2편에서는 설계한 테이블의 품질을 높이는 정규화(Normalization)를 다룰 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 설계에서 적용할 원칙은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대리키 PK, 자연키 UNIQUE&lt;/b&gt;: 비즈니스와 무관한 대리키를 PK로 사용하고, 비즈니스적으로 유일한 값에는 UNIQUE 제약조건을 적용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비식별 관계&lt;/b&gt;: 유연성과 확장성을 위해 FK를 PK의 일부로 사용하지 않는 비식별 관계를 선택합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1:1 관계의 FK 위치&lt;/b&gt;: 보조 테이블에 FK를 두고, 해당 FK에 UNIQUE 제약조건을 추가하여 1:1 관계를 강제합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NULL 제약조건으로 참여도 제어&lt;/b&gt;: FK의 NOT NULL 여부로 필수/선택 참여를 표현합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계할 테이블 목록은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;코드성 테이블&lt;/b&gt;: 이자종류, 상환방법, 대출상태&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 테이블&lt;/b&gt;: 회원, 주 계좌, 대출상품&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 테이블&lt;/b&gt;: 대출계약, 대출계좌, 대출상태 이력&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상환 테이블&lt;/b&gt;: 상환 스케줄, 상환 이력, 연체 이력&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1: 이자종류 , 상환방법 , 대출상품&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 이자종류 테이블&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 대출에 적용되는 이자 유형을 관리하는 코드성 테이블입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 종류id&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;1258&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhtQRT/dJMcagFMvHd/hLfU6gizdEY9RAa5fkbtrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhtQRT/dJMcagFMvHd/hLfU6gizdEY9RAa5fkbtrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhtQRT/dJMcagFMvHd/hLfU6gizdEY9RAa5fkbtrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhtQRT%2FdJMcagFMvHd%2FhLfU6gizdEY9RAa5fkbtrK%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;1258&quot; height=&quot;236&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이자종류명은 비지니스적으로 유일해야 하는 자연키 후보 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대리키 원칙에 따라 PK 는 시스템이 생성하는 종류id 로 두고, 이자종류명에는 UNIQUE 제약조건을 적용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 이자종류명이 변경되더라도 이를 참조하는 대출상품 테이블의 FK 는 아무런 영향을 받지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 상환방법 테이블&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 대출 상환 방식을 관리하는 코드성 테이블입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 방법id&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;1234&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yFHYE/dJMcahkop93/AVlxKtKAlIt2IUzvzjxJjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yFHYE/dJMcahkop93/AVlxKtKAlIt2IUzvzjxJjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yFHYE/dJMcahkop93/AVlxKtKAlIt2IUzvzjxJjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyFHYE%2FdJMcahkop93%2FAVlxKtKAlIt2IUzvzjxJjk%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;1234&quot; height=&quot;242&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이자종류 테이블과 동일한 패턴을 적용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 상품명이더라도 이자종류나 상환방법이 다르면 다른 상품으로 분류하기에 , 독립 테이블로 분리하여 대출상품에서 FK 로 참조하는 방식을 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 대출상품 테이블&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 대출 서비스에서 제공하는 상품 정보를 저장하는 테이블입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 상품id&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외래키(FK)&lt;/b&gt;: 이자종류id , 상환방법id&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cM2pUB/dJMcafz4FFm/lVrQhK19ntXMDbibsfHBqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cM2pUB/dJMcafz4FFm/lVrQhK19ntXMDbibsfHBqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cM2pUB/dJMcafz4FFm/lVrQhK19ntXMDbibsfHBqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcM2pUB%2FdJMcafz4FFm%2FlVrQhK19ntXMDbibsfHBqK%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;1240&quot; height=&quot;438&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품명에 UNIQUE 제약조건을 걸지 않았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계 상 , 같은 상품명이더라도 이자종류나 상환방법이 다른 상품이 각각 존재할 수 있기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개념적 모델링 -&amp;gt; 논리적 모델링 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boj2Y2/dJMcagsb4AG/mdcYjGa6BafhkSkDBrreBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boj2Y2/dJMcagsb4AG/mdcYjGa6BafhkSkDBrreBk/img.png&quot; data-alt=&quot;개념적 모델링&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boj2Y2/dJMcagsb4AG/mdcYjGa6BafhkSkDBrreBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fboj2Y2%2FdJMcagsb4AG%2FmdcYjGa6BafhkSkDBrreBk%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;506&quot; height=&quot;322&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개념적 모델링&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ApRp6/dJMcaftjKhz/QJgnb4K2RpoMTY4pOtkRXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ApRp6/dJMcaftjKhz/QJgnb4K2RpoMTY4pOtkRXk/img.png&quot; data-alt=&quot;논리적 모델링 - 테이블 설계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ApRp6/dJMcaftjKhz/QJgnb4K2RpoMTY4pOtkRXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FApRp6%2FdJMcaftjKhz%2FQJgnb4K2RpoMTY4pOtkRXk%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;521&quot; height=&quot;328&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;논리적 모델링 - 테이블 설계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2: 회원 , 주 계좌&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 회원 테이블&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 대출 서비스를 이용하는 회원 정보를 저장하는 테이블입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 회원id&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;1236&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tBKGG/dJMcahLpJOC/5IQqr9y2uW7XQiRkNvYfz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tBKGG/dJMcahLpJOC/5IQqr9y2uW7XQiRkNvYfz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tBKGG/dJMcahLpJOC/5IQqr9y2uW7XQiRkNvYfz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtBKGG%2FdJMcahLpJOC%2F5IQqr9y2uW7XQiRkNvYfz1%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;1236&quot; height=&quot;434&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대리키 원칙에 따라 PK 는 회원 id 로 두고 , 로그인 id 에는 UNIQUE 제약조건을 적용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 회원이 로그인 id 를 변경하더라도 이를 참조하는 대출계약 데이터는 아무런 영향을 받지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 주 계좌 테이블&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 회원의 주 계좌 정보를 저장하는 테이블입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 계좌id&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외래키(FK)&lt;/b&gt;: 회원id&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o7oAG/dJMcadPIrCQ/j85FkGmbX94rVVP3fwzGt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o7oAG/dJMcadPIrCQ/j85FkGmbX94rVVP3fwzGt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o7oAG/dJMcadPIrCQ/j85FkGmbX94rVVP3fwzGt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo7oAG%2FdJMcadPIrCQ%2Fj85FkGmbX94rVVP3fwzGt0%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;1232&quot; height=&quot;436&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원과 주 계좌는 1:1 관계 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원이 먼저 존재해야 주 계좌가 생성되는 흐름이므로, 회원이 주 테이블이고 주 계좌가 보조 테이블 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보조 테이블에 FK 를 두는 원칙에 따라 주 계좌 테이블에 회원 id KF 를 배치했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 id 에 UNIQUE 제약조건을 추가하여 한 회원이 두 개 이상의 주 계좌를 갖는 상황을 DB 레벨에서 방지 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개념적 모델링 -&amp;gt; 논리적 모델링 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci3Luy/dJMcah5KIsD/9ujG6zfufKq04Kgzk31Ifk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci3Luy/dJMcah5KIsD/9ujG6zfufKq04Kgzk31Ifk/img.png&quot; data-alt=&quot;개념적 모델링&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci3Luy/dJMcah5KIsD/9ujG6zfufKq04Kgzk31Ifk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci3Luy%2FdJMcah5KIsD%2F9ujG6zfufKq04Kgzk31Ifk%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;658&quot; height=&quot;299&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개념적 모델링&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YyaUS/dJMcabEpOOu/5uFeOydrT9F29zNhueYk4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YyaUS/dJMcabEpOOu/5uFeOydrT9F29zNhueYk4K/img.png&quot; data-alt=&quot;논리적 모델링 - 테이블 설계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YyaUS/dJMcabEpOOu/5uFeOydrT9F29zNhueYk4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYyaUS%2FdJMcabEpOOu%2F5uFeOydrT9F29zNhueYk4K%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;661&quot; height=&quot;305&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;논리적 모델링 - 테이블 설계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3: 대출계약 , 대출계좌&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 대출계약 테이블&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 회원이 특정 대출상품에 가입할 때 생성되는 계약 정보를 저장하는 테이블입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 계약id&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외래키(FK)&lt;/b&gt;: 회원id,상품id&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;1240&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qJJfe/dJMcafGRhzu/IyQEnvpwTaZMW19TVNMZj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qJJfe/dJMcafGRhzu/IyQEnvpwTaZMW19TVNMZj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qJJfe/dJMcafGRhzu/IyQEnvpwTaZMW19TVNMZj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqJJfe%2FdJMcafGRhzu%2FIyQEnvpwTaZMW19TVNMZj0%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;1240&quot; height=&quot;502&quot; data-origin-width=&quot;1240&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;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;회원과 대출상품의 M:N 관계를 해소하기 위한 연결 테이블 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 회원은 여러 대출 상품에 가입할 수 있고 , 하나의 대출 상품은 여러 회원에게 적용될 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출계약에서 회원id , 상품id 에 UNIQUE 제약조건을 걸지 않았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 회원이 같은 대출상품으로 여러 번 계약할 수 있는 비지니스 상황이 존재하기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 기존 대출을 상환한 후 동일 상품으로 재대출하는 경우가 이에 해당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계약대출원금은 계약시점의 대출 원금을 저장합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상환이 진행되어도 계약 당시 원금은 변하지 않아야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 대출계좌 테이블&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 대출계약이 실행된 이후 실제 대출 운영 정보를 저장하는 테이블입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 계좌id&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외래키(FK)&lt;/b&gt;: 계약id,주계좌id&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;1242&quot; data-origin-height=&quot;572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfaZ7W/dJMcaf05XG8/LMGg8snha9jcOngkUXq8B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfaZ7W/dJMcaf05XG8/LMGg8snha9jcOngkUXq8B1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfaZ7W/dJMcaf05XG8/LMGg8snha9jcOngkUXq8B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfaZ7W%2FdJMcaf05XG8%2FLMGg8snha9jcOngkUXq8B1%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;1242&quot; height=&quot;572&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;572&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출계약과 대출계좌는 1:1 관계 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출계약이 먼저 존재해야 대출계좌가 생성되는 흐름이므로 , 대출계약이 주 테이블이고 대출계좌가 보조 테이블 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보조 테이블에 FK 를 두는 원칙에 따라 대출계좌 테이블에 계약id FK 를 배치했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계약id 에 UNIQUE 제약조건을 추가하여 하나의 계약에 2개 이상의 대출계좌가 생성되는 상황을 DB 레벨에서 방지 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD 의 상환계좌번호 컬럼은 설계 과정에서 주계좌id FK 로 대체 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계좌번호 문자열을 직접 저장하면 계좌번호가 변경될 경우 수동으로 업데이트 해야 하고 , DB 가 데이터 정합성을 보장할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 주계좌id 를 FK 로 참조하면 FK 제약조건으로 정합성이 자동으로 보장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개념적 모델링 -&amp;gt; 논리적 모델링 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPkzyC/dJMcabqUB2J/NZwtSm2wrHNmzPiQky2Qb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPkzyC/dJMcabqUB2J/NZwtSm2wrHNmzPiQky2Qb0/img.png&quot; data-alt=&quot;개념적 모델링&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPkzyC/dJMcabqUB2J/NZwtSm2wrHNmzPiQky2Qb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPkzyC%2FdJMcabqUB2J%2FNZwtSm2wrHNmzPiQky2Qb0%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;637&quot; height=&quot;434&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;978&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개념적 모델링&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;1040&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co5xaq/dJMb990WWWS/xq2dO3LK47kAk5ibqJ36B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co5xaq/dJMb990WWWS/xq2dO3LK47kAk5ibqJ36B1/img.png&quot; data-alt=&quot;논리적 모델링 - 테이블 설계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co5xaq/dJMb990WWWS/xq2dO3LK47kAk5ibqJ36B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fco5xaq%2FdJMb990WWWS%2Fxq2dO3LK47kAk5ibqJ36B1%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;643&quot; height=&quot;479&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;1040&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;논리적 모델링 - 테이블 설계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3: 대출상태 , 대출상태이력&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 대출상태 테이블&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 대출계좌의 상태 유형을 관리하는 코드성 테이블입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 상태id&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;1242&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LiREX/dJMcab5uHXB/Bh1A3T7Ah2wRK9NLAn4u91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LiREX/dJMcab5uHXB/Bh1A3T7Ah2wRK9NLAn4u91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LiREX/dJMcab5uHXB/Bh1A3T7Ah2wRK9NLAn4u91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLiREX%2FdJMcab5uHXB%2FBh1A3T7Ah2wRK9NLAn4u91%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;1242&quot; height=&quot;246&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&lt;/b&gt;&lt;/p&gt;
&lt;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;상태명은 비지니스적으로 유일해야 하는 자연키 후보이므로 UNIQUE 제약조건을 적용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 상태명이 변경되더라도 이를 참조하는 대출상태 이력 테이블의 FK 는 아무런 영향을 받지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 대출상태 이력 테이블&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 대출계좌의 상태 변화 이력을 저장하는 테이블입니다. 대출계좌의 상태가 변경될 때마다 새로운 row가 추가됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 이력id&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외래키(FK)&lt;/b&gt;: 대출계좌id , 상태id&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;1244&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwt8Uv/dJMcadWwpMV/rwUgKYNv4WmNkxN9Whyzh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwt8Uv/dJMcadWwpMV/rwUgKYNv4WmNkxN9Whyzh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwt8Uv/dJMcadWwpMV/rwUgKYNv4WmNkxN9Whyzh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwt8Uv%2FdJMcadWwpMV%2FrwUgKYNv4WmNkxN9Whyzh0%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;1244&quot; height=&quot;438&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종료일에 NULL 을 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 진행 중인 상태는 아직 종료되지 않았으므로 종료일이 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 , 종료일 = NULL 은 현재 활성 상태임을 의미 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴을 활용하면 &quot;WHERE 종료일 IS NULL&quot; 조건으로 현재 활성 상태를 간단하게 조회할 수 있습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개념적 모델링 -&amp;gt; 논리적 모델링 &lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AuoXf/dJMcaicwYFM/MTwKauorX2vMEHnm5Wl8IK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AuoXf/dJMcaicwYFM/MTwKauorX2vMEHnm5Wl8IK/img.png&quot; data-alt=&quot;개념적 모델링&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AuoXf/dJMcaicwYFM/MTwKauorX2vMEHnm5Wl8IK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAuoXf%2FdJMcaicwYFM%2FMTwKauorX2vMEHnm5Wl8IK%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;780&quot; height=&quot;207&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개념적 모델링&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caX7vv/dJMcagsb8ed/dHm8osVPHjitEwLriq7BBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caX7vv/dJMcagsb8ed/dHm8osVPHjitEwLriq7BBk/img.png&quot; data-alt=&quot;논리적 모델링 - 테이블 설계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caX7vv/dJMcagsb8ed/dHm8osVPHjitEwLriq7BBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaX7vv%2FdJMcagsb8ed%2FdHm8osVPHjitEwLriq7BBk%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;784&quot; height=&quot;237&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;논리적 모델링 - 테이블 설계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4: 상환 스케줄 , 상환 상태 , 상환 이력 , 연체 이력&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 상환상태 테이블&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 상환 스케줄의 상태 유형을 관리하는 코드성 테이블입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 상태id&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p06Cg/dJMcaglpHZ4/KKRISKWPn1uqz4yieKKjf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p06Cg/dJMcaglpHZ4/KKRISKWPn1uqz4yieKKjf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p06Cg/dJMcaglpHZ4/KKRISKWPn1uqz4yieKKjf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp06Cg%2FdJMcaglpHZ4%2FKKRISKWPn1uqz4yieKKjf1%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;1246&quot; height=&quot;244&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이자종류,상환방법,대출상태 테이블과 동일한 패턴을 적용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재는 MVP 개발 단계이지만 , 서비스가 확장되면서 새로운 상태가 추가될 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 상태명이 변경될 경우, VARCHAR 컬럼으로 관리하면 해당 값이 저장된 모든 row 를 수정해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드성 테이블로 분리하면 상태 테이블의 row 하나만 수정하면 되므로 확장성과 유지보수성을 위해 독립 테이블로 분리했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 상환 스케줄 테이블&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 대출계좌의 회차별 상환 예정 정보를 저장하는 테이블입니다. 대출 실행 시점에 전체 상환 일정이 생성됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 스케줄id&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외래키(FK)&lt;/b&gt;: 대출계좌id , 상태id&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;1242&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/69YE0/dJMcab5uIiq/rfov49qyRVt7TjLKRSyKx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/69YE0/dJMcab5uIiq/rfov49qyRVt7TjLKRSyKx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/69YE0/dJMcab5uIiq/rfov49qyRVt7TjLKRSyKx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F69YE0%2FdJMcab5uIiq%2Frfov49qyRVt7TjLKRSyKx0%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;1242&quot; height=&quot;580&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출 계좌의 회차별 상환 예정 정보를 저장하는 테이블 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용금리는 상환 스케줄 생성 시점의 금리를 스냅샷으로 저장합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변동금리 상품의 경우 금리가 변경될 수 있으므로 , 각 회차별 실제 적용된 금리를 별도로 저장해야 과거 이력을 정확하게 추적할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 연체 종류 테이블&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 연체 이력의 연체 유형을 관리하는 코드성 테이블입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본키(PK) : 종류id&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAbFu6/dJMcaayODPu/CVKvatk3khoAEzkSk2vjjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAbFu6/dJMcaayODPu/CVKvatk3khoAEzkSk2vjjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAbFu6/dJMcaayODPu/CVKvatk3khoAEzkSk2vjjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAbFu6%2FdJMcaayODPu%2FCVKvatk3khoAEzkSk2vjjK%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;1242&quot; height=&quot;238&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이자종류, 상환방법, 대출상태, 상환상태 테이블과 동일한 패턴을 적용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연체 이력은 연체가 발생할 때마다 매일 row가 추가되는 테이블입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VARCHAR 컬럼으로 관리하면 오타나 잘못된 값이 저장될 위험이 있고, DB 레벨에서 유효한 값인지 검증할 수 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 연체 이력 테이블&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 연체가 발생한 상환 스케줄의 연체 정보를 저장하는 테이블입니다. 연체가 발생할 때마다 새로운 row가 추가됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본키(PK): 연체id&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외래키(FK): 스케줄id , 연체종류id&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;1244&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x3CNa/dJMcag6PuiQ/AHYwnh8BSQsSt20FqPdyN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x3CNa/dJMcag6PuiQ/AHYwnh8BSQsSt20FqPdyN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x3CNa/dJMcag6PuiQ/AHYwnh8BSQsSt20FqPdyN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx3CNa%2FdJMcag6PuiQ%2FAHYwnh8BSQsSt20FqPdyN0%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;1244&quot; height=&quot;504&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계노트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상환 스케줄과 연체 이력은 1:N 관계로, 하나의 스케줄에서 연체가 여러 번 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 1회차 스케줄에서 연체가 발생했다가 일부 해소되고, 이후 다시 연체가 발생하는 경우가 이에 해당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연체이자율은 연체 발생 시점의 이자율을 스냅샷으로 저장합니다. 이자율은 시간이 지남에 따라 변경될 수 있으므로, 연체가 발생한 시점의 이자율을 별도로 저장해야 과거 연체 이력을 정확하게 추적할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케줄id는 NOT NULL로 설정합니다. 연체는 반드시 특정 회차의 상환 스케줄에서 발생하며, 스케줄 없는 연체 이력은 비즈니스적으로 존재할 수 없기 때문입니다. 또한 연체 이력이 스케줄id를 FK로 보유하고 있기 때문에, 상환 이력에서 연체id를 선택적으로 참조하는 것만으로도 해당 연체의 스케줄 정보를 추적할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 상환 이력 테이블&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 실제 납부가 완료된 상환 내역을 저장하는 테이블입니다. 납부가 발생할 때마다 새로운 row가 추가됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK)&lt;/b&gt;: 이력id&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외래키(FK)&lt;/b&gt;: 스케줄id,연체id&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;1240&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFxcSN/dJMcahYVBK9/NxoNxQkdSQvtform7rANAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFxcSN/dJMcahYVBK9/NxoNxQkdSQvtform7rANAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFxcSN/dJMcahYVBK9/NxoNxQkdSQvtform7rANAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFxcSN%2FdJMcahYVBK9%2FNxoNxQkdSQvtform7rANAk%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;1240&quot; height=&quot;576&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&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;상환 스케줄과 상환 이력은 1:N 관계입니다.&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;스케줄id는 NOT NULL로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 납부는 반드시 특정 회차의 스케줄에 대한 납부이며, 스케줄 없는 납부 이력은 비즈니스적으로 존재할 수 없기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연체id는 NULL을 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 납부의 경우 연체 이력이 존재하지 않으므로 선택적으로 연결됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연체가 발생한 납부의 경우에만 연체 이력 테이블의 연체id를 참조합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설계를 통해 &quot;연체id IS NULL&quot; 조건으로 정상 납부 이력을, &quot;연체id IS NOT NULL&quot; 조건으로 연체 납부 이력을 간단하게 구분할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;납부연체금은 정상 납부의 경우 0으로 저장됩니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개념적 모델링 -&amp;gt; 논리적 모델링 &lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/peW8I/dJMcaaMgS8W/l4wlQow6Jkd5VRDOgUKdP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/peW8I/dJMcaaMgS8W/l4wlQow6Jkd5VRDOgUKdP0/img.png&quot; data-alt=&quot;개념적 모델링&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/peW8I/dJMcaaMgS8W/l4wlQow6Jkd5VRDOgUKdP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpeW8I%2FdJMcaaMgS8W%2Fl4wlQow6Jkd5VRDOgUKdP0%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;788&quot; height=&quot;457&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개념적 모델링&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;1372&quot; data-origin-height=&quot;1280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lgpIp/dJMcaiwOWl1/Tf6jb74nLIBGoXllV8mXB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lgpIp/dJMcaiwOWl1/Tf6jb74nLIBGoXllV8mXB0/img.png&quot; data-alt=&quot;논리적 모델링 - 테이블 설계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lgpIp/dJMcaiwOWl1/Tf6jb74nLIBGoXllV8mXB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlgpIp%2FdJMcaiwOWl1%2FTf6jb74nLIBGoXllV8mXB0%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;783&quot; height=&quot;730&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;1280&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;논리적 모델링 - 테이블 설계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;논리적 모델링 - 테이블 설계 결과&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;1270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HYJc2/dJMcaf08b9C/n7GUskyYtnZDaLbiYffe01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HYJc2/dJMcaf08b9C/n7GUskyYtnZDaLbiYffe01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HYJc2/dJMcaf08b9C/n7GUskyYtnZDaLbiYffe01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHYJc2%2FdJMcaf08b9C%2Fn7GUskyYtnZDaLbiYffe01%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;1258&quot; height=&quot;1270&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;1270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 대출 도메인의 개념적 모델을 바탕으로 논리적 모델링 1편, 테이블 설계를 완성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 설계한 테이블 목록은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;코드성 테이블&lt;/b&gt;: 이자종류, 상환방법, 대출상태, 상환상태, 연체상태&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 테이블&lt;/b&gt;: 회원, 주 계좌, 대출상품&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 테이블&lt;/b&gt;: 대출계약, 대출계좌, 대출상태 이력&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상환 테이블&lt;/b&gt;: 상환 스케줄, 상환 이력, 연체 이력&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이 논리적 모델의 품질을 한 단계 더 끌어올려 데이터 중복을 최소화하고 무결성을 극대화하는 과정인 정규화를 진행하겠습니다.&lt;/p&gt;</description>
      <category>프로젝트/대출금 상환 프로젝트</category>
      <author>watanabe3</author>
      <guid isPermaLink="true">https://loan-project.tistory.com/28</guid>
      <comments>https://loan-project.tistory.com/28#entry28comment</comments>
      <pubDate>Wed, 3 Jun 2026 16:55:34 +0900</pubDate>
    </item>
    <item>
      <title>[3] 데이터베이스 개념적 모델링</title>
      <link>https://loan-project.tistory.com/27</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;대출금 상환 프로그램 - 개념적 모델링&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터베이스 설계는 보통 3단계를 거칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;개념적 모델링&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비지니스 요구사항을 보면서 엔티티 , 엔티티의 속성 , 엔티티의 관계를 정의하는 단계 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 데이터베이스 기술은 고려하지 않고 , ERD 로 핵심 구조를 표현하는 것이 목표 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;논리적 모델링&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD 를 관계형 데이터베이스에 맞게 구체화 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 , 컬럼 , 기본키 , 외래키를 정의하고 정규화를 진행 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;물리적 모델링&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사용할 데이터베이스(MySQL , Oracle 등)의 특성에 맞게 인덱스 , 데이터 타입 , 파티셔닝 등 성능과 저장 방식을 결정 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 글은 그 첫번째 단계인 개념적 모델링을 다룹니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대출 실행은 이미 완료된 상태라고 가정하고 , 대출금 상환 과정에서 필요한 엔티티와 관계를 하나씩 도출해 나간 과정을 담았습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;뿌리 엔티티 1 - &lt;b&gt;회원과 주 계좌&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;요구사항&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원은 회원 ID , 로그인 ID , 비밀번호 , 이름 , 가입일시 정보를 가집니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;계좌는 계좌 ID , 계좌번호 , 잔액 , 생성일시 정보를 가집니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;회원은 가입 시 반드시 하나의 주 계좌를 가져야 합니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;관계&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 명의 회원은 반드시 하나의 주 계좌를 가지고 , 계좌는 반드시 한 명의 회원에 속합니다.회원과 계좌는 1:1 필수 관계 입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ERD&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&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;770&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cohthk/dJMcadhVBlX/2bciFKaXefIMYIHtKtkbw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cohthk/dJMcadhVBlX/2bciFKaXefIMYIHtKtkbw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cohthk/dJMcadhVBlX/2bciFKaXefIMYIHtKtkbw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcohthk%2FdJMcadhVBlX%2F2bciFKaXefIMYIHtKtkbw0%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;770&quot; height=&quot;348&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;뿌리 엔티티 2 - &lt;span&gt;대출상품 , 이자종류 , 상환방법&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;요구사항&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대출상품은 상품 ID, 상품명, 가산금리 정보를 가집니다.&lt;/li&gt;
&lt;li&gt;이자종류는 종류 ID, 이자종류명 정보를 가집니다. 이자종류는 고정금리, 변동금리, 혼합금리 3가지로 구분됩니다.&lt;/li&gt;
&lt;li&gt;상환방법은 방법 ID, 상환방법명 정보를 가집니다. 상환방법은 원리금균등상환, 원금균등상환, 만기일시상환 3가지로 구분됩니다.&lt;/li&gt;
&lt;li&gt;대출상품은 반드시 하나의 이자종류와 상환방법을 가져야 합니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 대출상품은 반드시 하나의 이자종류를 가집니다. 하나의 이자종류는 여러 대출상품에 적용될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이자종류와 대출상품은 1:N 관계이며, 대출상품 입장에서 이자종류는 필수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 대출상품은 반드시 하나의 상환방법을 가집니다. 하나의 상환방법은 여러 대출상품에 적용될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 상환방법과 대출상품은 1:N 관계이며, 대출상품 입장에서 상환방법은 필수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ERD&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;806&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7OiWZ/dJMcadhVBQR/41NNS2ogVJESlwmbCERUKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7OiWZ/dJMcadhVBQR/41NNS2ogVJESlwmbCERUKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7OiWZ/dJMcadhVBQR/41NNS2ogVJESlwmbCERUKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7OiWZ%2FdJMcadhVBQR%2F41NNS2ogVJESlwmbCERUKk%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;806&quot; height=&quot;498&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;M:N 해소 - 대출계약&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;요구사항&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한명의 회원은 동시에 여러 대출상품에 가입할 수 있습니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;하나의 대출상품은 여러 회원에게 가입될 수 있습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;M:N 관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 요구사항을 ERD 로 표현하면 회원과 대출상품 사이에 M:N 관계가 생깁니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;1932&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qTiL0/dJMb997DQ8Z/IMuSZ2XeTcVHDwKQduzHk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qTiL0/dJMb997DQ8Z/IMuSZ2XeTcVHDwKQduzHk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qTiL0/dJMb997DQ8Z/IMuSZ2XeTcVHDwKQduzHk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqTiL0%2FdJMb997DQ8Z%2FIMuSZ2XeTcVHDwKQduzHk1%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;1932&quot; height=&quot;510&quot; data-origin-width=&quot;1932&quot; data-origin-height=&quot;510&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;M:N 관계는 2가지 문제를 가집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 관계형 데이터베이스에서 물리적으로 구현할 수 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 관계 속에서만 의미를 갖는 데이터를 저장할 곳이 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;대출계약 엔티티 도입 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해소하기 위해 대출계약 엔티티를 도입합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출계약은 회원과 대출상품 사이의 M:N 관계를 2개의 1:N 관계로 풀어줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2270&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4l18I/dJMcafNCio3/KY08AKKnAp5q8AvdFibk01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4l18I/dJMcafNCio3/KY08AKKnAp5q8AvdFibk01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4l18I/dJMcafNCio3/KY08AKKnAp5q8AvdFibk01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4l18I%2FdJMcafNCio3%2FKY08AKKnAp5q8AvdFibk01%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;2270&quot; height=&quot;516&quot; data-origin-width=&quot;2270&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;대출계약과 대출계좌&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;요구사항&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대출계약은 대출 주기에서 변하지 않는 정보인 계약 ID , 계약일 , 만기일 , 계약대출 원금 정보를 가집니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;대출계좌는 대출 주기에서 변하는 정보인 계좌 ID , 계좌번호 , 대출원금잔액 , 상환계좌번호 , 매달 상환일 , 현재 회차를 가집니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;대출계약이 체결되면 반드시 하나의 대출계좌가 생성되어야 합니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출 계좌는 대출 계약에서 파생된 엔티티 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 대출계약은 반드시 하나의 대출계좌를 가져야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 대출계약과 대출계좌는&amp;nbsp;1:1 필수 관계&amp;nbsp;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ERD&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;886&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BiP5Q/dJMcah5ICCs/rTibpNzm93AYGVsfSbmkW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BiP5Q/dJMcah5ICCs/rTibpNzm93AYGVsfSbmkW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BiP5Q/dJMcah5ICCs/rTibpNzm93AYGVsfSbmkW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBiP5Q%2FdJMcah5ICCs%2FrTibpNzm93AYGVsfSbmkW1%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;886&quot; height=&quot;410&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;대출계좌와 대출상태&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대출계좌는 상환이 진행되면서 상태가 변하는데 상태 변화 이력을 추적해야 합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;대출상태는 상태 ID, 상태명 정보를 가집니다.&lt;/li&gt;
&lt;li&gt;대출상태 이력은 이력 ID, 시작일, 종료일 정보를 가집니다.&lt;/li&gt;
&lt;li&gt;대출계좌는 여러 개의 대출상태 이력을 가질 수 있으며, 하나의 대출상태 이력은 반드시 하나의 대출계좌에 속합니다.&lt;/li&gt;
&lt;li&gt;하나의 대출상태는 여러 대출상태 이력에 연결될 수 있으며, 하나의 대출상태 이력은 반드시 하나의 대출상태를 가집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출계좌와 대출상태 이력은 1:N 필수 관계입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출상태와 대출상태 이력은 1:N 관계이며, 대출상태 이력 입장에서 대출상태는 필수입니다.&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;ERD&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rQiCk/dJMcaicuUQw/rsbElKAV1LPN2lKPeclUB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rQiCk/dJMcaicuUQw/rsbElKAV1LPN2lKPeclUB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rQiCk/dJMcaicuUQw/rsbElKAV1LPN2lKPeclUB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrQiCk%2FdJMcaicuUQw%2FrsbElKAV1LPN2lKPeclUB1%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;1198&quot; height=&quot;404&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;대출계좌에서 파생 - 상환스케줄&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대출 계좌에 대해 언제 , 얼마를 상환 할지에 대한 상환 스케줄 데이터가 필요합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;상환 스케줄은 스케줄 ID , 예정일 , 상환금(원금,이자) , 적용금리 정보를 가집니다.&lt;/li&gt;
&lt;li&gt;하나의 대출 계좌에 대해 1개 이상의 상환 스케줄을 가질 수 있으며 , 하나의 상환 스케줄은 반드시 하나의 대출계좌에 속합니다.&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 대출계좌는 여러 개의 상환 스케줄을 가질 수 있으며, 하나의 상환 스케줄은 반드시 하나의 대출계좌에 속합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 대출계좌와 상환 스케줄은 &lt;b&gt;1:N 필수 관계&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ERD&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bakdrw/dJMcagFKvRU/WkpkRDLK76JfZAXKkh6lx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bakdrw/dJMcagFKvRU/WkpkRDLK76JfZAXKkh6lx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bakdrw/dJMcagFKvRU/WkpkRDLK76JfZAXKkh6lx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbakdrw%2FdJMcagFKvRU%2FWkpkRDLK76JfZAXKkh6lx1%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;934&quot; height=&quot;398&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;상환스케줄에서 파생 - 상환이력 / 연체&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상환 스케줄에 대해 연체할 경우 이에 대해 매일 이력을 기록할 데이터가 필요합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;연체 이력은 연체 ID, 연체 종류, 연체 일시, 연체 이자율, 연체금 정보를 가집니다.&lt;/li&gt;
&lt;li&gt;상환 스케줄과 연체 이력에 대해 실제로 얼마를 상환했는지 기록할 데이터가 필요 합니다.&lt;/li&gt;
&lt;li&gt;상환 이력은 이력 ID, 납부 원금, 납부 이자, 납부 연체금, 납부 일시 정보를 가집니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상환 스케줄과 상환 이력은 &lt;b&gt;1:N 선택적 관계&lt;/b&gt;입니다. 하나의 상환 스케줄에 대해 여러 번 나눠서 납부할 수 있으며, 정상 납부된 스케줄은 상환 이력이 없을 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상환 스케줄과 연체 이력은 &lt;b&gt;1:N 선택적 관계&lt;/b&gt;입니다. 정상 납부된 스케줄은 연체 이력이 없으며, 미납 시 매일 연체 이력이 쌓입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연체 이력과 상환 이력은 &lt;b&gt;1:N 선택적 관계&lt;/b&gt;입니다. 연체금을 납부할 때 어떤 연체 이력에 대한 납부인지 연결됩니다. 연체금 납부가 없으면 상환 이력과 연결되지 않을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ERD&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFnZ0l/dJMcagMv4r1/2G8JpnknLbKKnjnwKi3xkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFnZ0l/dJMcagMv4r1/2G8JpnknLbKKnjnwKi3xkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFnZ0l/dJMcagMv4r1/2G8JpnknLbKKnjnwKi3xkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFnZ0l%2FdJMcagMv4r1%2F2G8JpnknLbKKnjnwKi3xkk%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;950&quot; height=&quot;832&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;832&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;최종&amp;nbsp;ERD&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 요구사항을 바탕으로 엔티티를 하나씩 도출하고, 관계를 연결해왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿌리 엔티티에서 시작해 M:N 관계를 해소하고, 대출계약에서 대출계좌, 상환 스케줄, 상환 이력과 연체 이력까지 파생되는 흐름을 따라가다 보면 아래 ERD가 완성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2018&quot; data-origin-height=&quot;1150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgUG1g/dJMcageDE6O/paBeBoif8EvbHdYb8jBgEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgUG1g/dJMcageDE6O/paBeBoif8EvbHdYb8jBgEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgUG1g/dJMcageDE6O/paBeBoif8EvbHdYb8jBgEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgUG1g%2FdJMcageDE6O%2FpaBeBoif8EvbHdYb8jBgEk%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;2018&quot; height=&quot;1150&quot; data-origin-width=&quot;2018&quot; data-origin-height=&quot;1150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적 모델링 단계에서는 특정 데이터베이스 기술에 종속되지 않고, 비즈니스 요구사항을 중심으로 데이터의 구조와 관계를 정의하는 것이 목표였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이 개념적 모델을 바탕으로 논리적 모델링을 진행해보겠습니다.&lt;/p&gt;</description>
      <category>프로젝트/대출금 상환 프로젝트</category>
      <author>watanabe3</author>
      <guid isPermaLink="true">https://loan-project.tistory.com/27</guid>
      <comments>https://loan-project.tistory.com/27#entry27comment</comments>
      <pubDate>Sun, 31 May 2026 15:20:31 +0900</pubDate>
    </item>
    <item>
      <title>[2] 가계 대출 상품 설명서 분석 - 2</title>
      <link>https://loan-project.tistory.com/26</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;5. 중도 상환 수수료&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MBVnm/dJMcafml8La/gWQBmlbCBQeXmunfdKHuCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MBVnm/dJMcafml8La/gWQBmlbCBQeXmunfdKHuCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MBVnm/dJMcafml8La/gWQBmlbCBQeXmunfdKHuCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMBVnm%2FdJMcafml8La%2FgWQBmlbCBQeXmunfdKHuCK%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;1244&quot; height=&quot;494&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;494&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;약관해석&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;중도 상환 수수료란 , 대출 만기 전에 미리 갚을 때 고객이 부담하는 수수료 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;계산 공식은 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;중도 상환 수수료 = 중도 상환 대출 금액 X 중도 상환 수수료율(%) X (대출 잔여일수 / 대출 기간)&amp;nbsp;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;쉽게 말하면 얼마를 갚느냐 X 수수료율 X 얼마나 일찍 갚느냐의 비율로 계산됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;대출 만기에 가까울수록 잔여일수가 줄어들기 때문에 수수료 부담도 줄어드는 구조 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;면제 조건도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최초 대출 취급일로부터 3년이 지나면 수수료가 부과되지 않습니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;대출 만기 3개월 이내에 상환하는 경우 면제 됩니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;기존 대출을 해지하고 동일 은행에서 사실상 동일한 계약을 새로 체결한 경우, 두 계약의 유지기간 합산이 3년을 초과하면 면제됩니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아이디어&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(1) 중도 상환 수수료 계산 로직&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대출 잔여 일수는 상환일 기준으로 만기일 까지 남은 일수로 계산 해야 합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;대출 기간은 일 단위로 환산이 필요 합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;수수료율은 대출 상품별로 다르게 관리되어야 합니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(2) 면제 조건 분기 처리&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;면제 조건을 정리하면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;①&lt;/td&gt;
&lt;td&gt;현재 계약의 대출 취급일로부터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;3년 경과&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;대환 없이 동일 계약을 3년 이상 유지한 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;②&lt;/td&gt;
&lt;td&gt;만기일까지&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;잔여일수 90일 이하&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;만기가 3개월 이내로 임박한 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③&lt;/td&gt;
&lt;td&gt;대환 계약 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이전 계약 유지기간과 합산하여 3년 초과&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;동일 은행 내 대환이력이 있을 때, 현재 계약만으로는 3년 미만이더라도 이전 계약 기간을 합산해서 3년을 넘으면 면제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;개발 관점에서는 아래와 같이 체크해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780206617590&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;1. 잔여일수 &amp;le; 90일? &amp;rarr; 면제
2. 현재 계약 취급일로부터 3년 경과? &amp;rarr; 면제
3. 동일 은행 대환 이력 있음?
   └─ 이전 계약 유지기간 합산 &amp;rarr; 3년 초과? &amp;rarr; 면제
4. 위 조건 모두 해당 없음 &amp;rarr; 수수료 계산&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;3번에서 대환 이력을 조회해야 하므로 , 대출 대환 계약시 , 계약간 연결관계를 DB 에 저장하는 것도 고려 해볼 수 있습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 대출계약 철회권&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Br5d/dJMcajoD6ta/RYba8M4KBRmjZy291IM5Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Br5d/dJMcajoD6ta/RYba8M4KBRmjZy291IM5Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Br5d/dJMcajoD6ta/RYba8M4KBRmjZy291IM5Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Br5d%2FdJMcajoD6ta%2FRYba8M4KBRmjZy291IM5Kk%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;1242&quot; height=&quot;562&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;약관해석&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;대출계약 철회권이란, 대출을 받은 고객이 일정 기간 내에 계약 자체를 없었던 것으로 되돌릴 수 있는 권리 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;단순 상환이 아니라 계약 자체를 취소하는 개념 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;철회 가능 기간은 아래 3가지 날짜 중 가장 나중에 발생한 날로부터 14일 이내 입니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계약 체결일&lt;/li&gt;
&lt;li&gt;계약 서류를 받은 날&lt;/li&gt;
&lt;li&gt;대출금 수령일&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(1) 철회 시 고객이 반환해야 하는 것&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;은행이 제 3자에게 부담한 인지세 , 근저당권 설정 비용 등&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(2) 철회권 행사 시 혜택&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중도 상환 수수료 면제&lt;/li&gt;
&lt;li&gt;5영업일 이내 대출 관련 신용 정보 삭제&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(3) 남용 방지 조항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일 은행에서 1개월 내 동일유형 대출을 2회 이상 철회하면 신규 대출 거절 , 대출한도 축소 , 금리 우대 제한 등의 불이익 발생 가능&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아이디어&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(1) 철회 가능 기간 계산&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;3가지 날짜 중 가장 늦은 날을 기준으로 14일을 계산해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780206617591&quot; class=&quot;haxe&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;철회_기준일 = MAX(계약체결일, 계약서류수령일, 대출금수령일)
철회_만료일 = 철회_기준일 + 14일
// 단, 만료일이 휴일이면 다음 영업일로 연장&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(2) 철회 처리시 반환 금액 계산&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780206617592&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;반환금액 = 대출원금 + 발생이자 + 제3자 부담비용(인지세, 근저당권설정비용 등)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;대출 계약에 대해 제 3자 부담 비용을 별도 항목으로 저장 해두어야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(3) 대출 계약 삭제&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;대출 실행시 , 일어나는 대출 계좌 , 대출 상환 스케줄 , 대출 계약이 삭제될 수 있어야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(4) 남용 감지 로직&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;동일 고객 + 동일 대출유형 + 1개월 내 철회 2회 이상&lt;/td&gt;
&lt;td&gt;불이익 플래그 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;철회 이력 테이블에 고객 ID , 대출 유형 , 철회 일시를 기록해두고 , 신규 대출 신청 시점에 조회하는 구조가 필요 합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 금리 인하 요구권&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RWnD8/dJMcaipUq7u/rDEkYf8wlW5rciEh4CVgkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RWnD8/dJMcaipUq7u/rDEkYf8wlW5rciEh4CVgkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RWnD8/dJMcaipUq7u/rDEkYf8wlW5rciEh4CVgkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRWnD8%2FdJMcaipUq7u%2FrDEkYf8wlW5rciEh4CVgkK%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;1272&quot; height=&quot;482&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;약관해석&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;금리인하요구권이란 ?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;신용상태가 개선된 금융소비자가 은행에 금리를 낮춰달라고 요구할 수 있는 권리 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 신청 방법과 횟수&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;영업점 방문 또는 인터넷뱅킹 , 모바일뱅킹으로 신청 가능합니다. 횟수 제한이 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;신용상태가 개선될 때마다 반복 신청할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 은행의 응답 의무&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;요구를 받은 날로부터 10영업일 이내에 수용 여부와 사유를 통보해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단 , 고객에게 자료 보완을 요구한 기간은 10영업일에서 제외됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사실상 자료 제출이 늦어지면 그만큼 처리도 늦어지는 구조 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(3) 실제 금리 적용 시점&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;수용된다고 해서 바로 낮아지는 게 아닙니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;약정 이후에 실제 인하된 금리가 적용 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아이디어&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 신청 가능 여부 판단&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대출 상품 유형을 구분해서 관리해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;신용 상태가 금리에 영향을 미치지 않는 상품은 신청 자체를 막아야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(2) 10영업일 처리 기한 관리&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;신청일 기준으로 10 영업일을 계산해야 하는데, 은행 영업일 기준이라 공휴일과 주말을 제외한 날짜 계산 로직이 필요 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(3) 신청 상태 관리&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;신청 -&amp;gt; 검토 중 -&amp;gt; 수용/거절 의 흐름을 상태로 관리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한 수용됐을 때도 약정이 완료되어 금리가 변경되었는지도 상태를 관리해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 채무변제충당순서 변경 신청&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biM9yA/dJMcabK8S5c/kimbHVInvegEssKAnvoAAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biM9yA/dJMcabK8S5c/kimbHVInvegEssKAnvoAAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biM9yA/dJMcabK8S5c/kimbHVInvegEssKAnvoAAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiM9yA%2FdJMcabK8S5c%2FkimbHVInvegEssKAnvoAAk%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;1272&quot; height=&quot;266&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;약관해석&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;채무변제충당순서란 ?&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;대출 상환 시, 납부한 돈을 어느 항목에 먼저 적용할지의 순서 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;고객이 그달에 갚아야 하는 돈을 전부 갚지 못하고 일부만 갚을 경우 , 어느 항목에 대한 돈 부터 먼저 충당 되는지 순서를 나타냅니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;대출 상환 시 발생하는 항목에는 3가지가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비용 : 연체 관련 비용&lt;/li&gt;
&lt;li&gt;이자 : 약정 이자&lt;/li&gt;
&lt;li&gt;원금 : 실제 빌린 돈&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;882&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qGzCj/dJMcab5nOYl/KK4u8tPlCUUxMYHXVND5z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qGzCj/dJMcab5nOYl/KK4u8tPlCUUxMYHXVND5z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qGzCj/dJMcab5nOYl/KK4u8tPlCUUxMYHXVND5z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqGzCj%2FdJMcab5nOYl%2FKK4u8tPlCUUxMYHXVND5z0%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;882&quot; height=&quot;328&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;이러한 채무변제충당순서는 기한이익이 상실된 대출(대출 잔여 원금에 대해 연체이자가 부과되는 상태)에 한하여 변경을 신청할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아이디어&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(1) 상환 시 차감 순서를 코드로 표현&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;납입금이 들어왔을 때, 비용 -&amp;gt; 이자 -&amp;gt; 원금 혹은 비용 -&amp;gt; 원금 -&amp;gt; 이자 순으로 잔액을 차감하는 로직이 필요 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(2) 대출 상태 구분&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;정상 상태 , 단순 연체 상태 , 기한 이익 상실 상태를 구분하여 대출 계좌의 상태를 관리해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;기한이익상실 상태일 때만 충당 순서 변경을 허용하는 조건이 필요합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(3) 충당 순서 변경 처리&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;순서가 바뀌면 같은 납입금이라도 원금이 먼저 줄어드는 방식으로 동작해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;두 가지 충당 방식을 전환할 수 있는 구조가 필요합니다.&lt;/p&gt;</description>
      <category>프로젝트/대출금 상환 프로젝트</category>
      <author>watanabe3</author>
      <guid isPermaLink="true">https://loan-project.tistory.com/26</guid>
      <comments>https://loan-project.tistory.com/26#entry26comment</comments>
      <pubDate>Sun, 17 May 2026 17:22:09 +0900</pubDate>
    </item>
    <item>
      <title>[2] 가계 대출 상품 설명서 분석 - MVP 기능</title>
      <link>https://loan-project.tistory.com/25</link>
      <description>&lt;p data-ke-size=&quot;size18&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;1254&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwB5eI/dJMcabjYoAQ/bVWHZqQcm7E5DMk5Wyi101/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwB5eI/dJMcabjYoAQ/bVWHZqQcm7E5DMk5Wyi101/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwB5eI/dJMcabjYoAQ/bVWHZqQcm7E5DMk5Wyi101/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwB5eI%2FdJMcabjYoAQ%2FbVWHZqQcm7E5DMk5Wyi101%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;1254&quot; height=&quot;358&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;은행 웹사이트의 금융 상품 페이지에서 대출을 검색하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;가계대출 상품 설명서&lt;/b&gt;를 찾아볼 수 있습니다. 이 설명서는 대출을 처음 접하는 고객도 쉽게 이해할 수 있도록 은행이 직접 검수하고 정제한 문서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대출 도메인에 익숙하지 않은 개발자 입장에서, 대출금 상환 프로그램을 구현하는 일은 생각보다 낯설고 막막한 작업이었습니다. 그때, 이 상품 설명서가 뜻밖에 훌륭한 기술 명세서 역할을 해줬습니다. 비즈니스 요구사항에 대한 힌트가 잘 정리되어 있었고, 구현에 필요한 핵심 로직의 단서들도 곳곳에 담겨 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 시리즈는 두 편으로 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1편(이 글)에서는 MVP 기능에 해당하는 요구사항을 먼저 정리합니다.&lt;/b&gt; 대출 금리, 상환 방법, 연체 이자율, 연체금 결정, 채무변제충당순서가 그 대상입니다.&amp;nbsp; 이를 바탕으로 핵심 기능을 빠르게 구현한 뒤,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2편에서 중도 상환, 대출 계약 철회, 금리인하 요구, 채무변제충당순서 변경, 대환 등 추가 요구사항을 이어서 다룰 예정입니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;1. 대출 금리&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NrjEZ/dJMcabK3tt8/rQjeHS5zuukyXsmN5CdHzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NrjEZ/dJMcabK3tt8/rQjeHS5zuukyXsmN5CdHzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NrjEZ/dJMcabK3tt8/rQjeHS5zuukyXsmN5CdHzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNrjEZ%2FdJMcabK3tt8%2FrQjeHS5zuukyXsmN5CdHzk%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;1246&quot; height=&quot;768&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;약관해석&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(1) 금리 유형 3가지&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 71px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 18px;&quot;&gt;고정금리&lt;/td&gt;
&lt;td style=&quot;width: 57.907%; height: 18px;&quot;&gt;대출 실행 시 결정된 금리가 만기까지 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 18px;&quot;&gt;변동금리&lt;/td&gt;
&lt;td style=&quot;width: 57.907%; height: 18px;&quot;&gt;일정주기(3/6/12개월)마다 기준금리 변동에 따라 대출금리 변동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 18px;&quot;&gt;혼합금리&lt;/td&gt;
&lt;td style=&quot;width: 57.907%; height: 18px;&quot;&gt;고정금리 + 변동금리 결합 형태&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(2) 최종 금리 계산 구조&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779006221829&quot; class=&quot;gcode&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;최종금리 = 대출기준금리
         + 가산금리 (리스크프리미엄 + 유동성프리미엄 + 신용프리미엄 + 자본비용 + 업무원가 + 법적비용 + 목표이익률)
         &amp;plusmn; 가감조정 전결금리 (부수거래 감면 + 본부조정 + 영업점장 전결 조정)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아이디어 &lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(1) 금리 유형에 따른 상환 금액 결정&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;금리 유형에 따라 상환 금액이 달라져야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779006221829&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;if 고정금리:
    만기까지 최초 약정금리 그대로 적용

if 변동금리:
    변동주기(3/6/12개월)마다 기준금리 재조회 후 금리 갱신

if 혼합금리:
    고정기간 동안은 고정금리 적용
    고정기간 종료 후 변동금리로 전환&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(2) 금리 저장 방식&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;최종 금리를 하나의 값만으로 저장하면 추적이 불가능 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;금리의 각 요소를 분리해서 저장해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(3) 기준 금리 변동 이력 관리&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;변동 금리 및 혼합 금리 상품의 경우 , 기준 금리가 바뀔 때마다 변경 이력을 쌓아야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;특정 시점의 금리를 조회할 수 있어야 이자 계산 및 분쟁 대응이 가능 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 상환 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btZHI5/dJMcacb4t3Z/MkPkFS5jQTdkSJj7aYF95K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btZHI5/dJMcacb4t3Z/MkPkFS5jQTdkSJj7aYF95K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btZHI5/dJMcacb4t3Z/MkPkFS5jQTdkSJj7aYF95K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtZHI5%2FdJMcacb4t3Z%2FMkPkFS5jQTdkSJj7aYF95K%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;1254&quot; height=&quot;490&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;약관해석&amp;nbsp;&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;원리금 균등상환&lt;/td&gt;
&lt;td&gt;매월&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;원금 + 이자 합계&lt;/b&gt;가 동일&lt;/td&gt;
&lt;td&gt;초기엔 이자 비중이 높고, 갈수록 원금 비중 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;원금 균등상환&lt;/td&gt;
&lt;td&gt;매월&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;원금&lt;/b&gt;이 동일&lt;/td&gt;
&lt;td&gt;이자는 잔여 원금 기준으로 계산되어 매월 납입액이 줄어듦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;만기 일시상환&lt;/td&gt;
&lt;td&gt;대출기간 중&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이자만 납부&lt;/b&gt;, 만기에 원금 전액 상환&lt;/td&gt;
&lt;td&gt;총 이자 부담이 가장 크지만 중도 월 납입 부담은 가장 적음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아이디어&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;금리 유형(고정/변동) X 상환방법(3가지) 에 따라 스케줄 원금 , 이자 금액 계산 로직이 달라져야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;① 고정금리 + 원리금 균등상환&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;대출 실행 시점에 갚아야 하는 돈을 계산하여 확정할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779006221831&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;r = 연이자율 / 12  (월 이자율)
n = 총 납입 횟수

월 납입액 = P &amp;times; [r(1+r)^n] / [(1+r)^n - 1]

매월 이자 = 잔여원금 &amp;times; r
매월 원금 = 월 납입액 - 매월 이자
잔여원금  = 전월 잔여원금 - 매월 원금&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;② 고정금리 + 원금 균등상환&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;원금은 고정이지만 이자는 매달 잔여 원금 기준으로 줄어듭니다. 역시 갚아야 하는 돈을&amp;nbsp;미리 확정 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779006221831&quot; class=&quot;makefile&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;r = 연이자율 / 12  (월 이자율)
n = 총 납입 횟수

매월 원금  = P / n
매월 이자  = 잔여원금 &amp;times; r
월 납입액  = 매월 원금 + 매월 이자
잔여원금   = 전월 잔여원금 - 매월 원금&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;③ 고정금리 + 만기 일시상환&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;중도에는 이자만, 만기에는 원금 전액을 상환 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779006221831&quot; class=&quot;gcode&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;r = 연이자율 / 12  (월 이자율)

매월 이자        = P &amp;times; r
월 납입액 (중도) = P &amp;times; r
월 납입액 (만기) = P + (P &amp;times; r)
잔여원금         = 만기 전까지 유지&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;④ 변동금리 + 원리금 균등상환&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;금리가 변동주기마다 바뀌기 때문에 변동 시점마다 잔여 원금 기준으로 월 납입액을 재계산 해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779006221831&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;변동주기 도래 시:
  1. 새 기준금리 조회
  2. 현재 잔여원금 확인
  3. 잔여 납입횟수 기준으로 월 납입액 재계산
  4. 이후 스케줄 전체 업데이트

&amp;rarr; 변동주기마다 스케줄 재생성 필요
&amp;rarr; 이전 스케줄 이력은 보존 (분쟁 대응)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;⑤ 변동금리 + 원금 균등상환&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;매월 원금은 고정이지만, 이자 계산에 쓰이는 금리가 변동 주기마다 바뀝니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779006221832&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;변동주기 도래 시:
  1. 새 기준금리 조회
  2. 매월 원금은 그대로 유지
  3. 이자 = 잔여원금 &amp;times; 새 월 이자율 로 재계산
  4. 이후 스케줄 이자 항목만 업데이트

&amp;rarr; ④보다 재계산 범위가 작음 (원금 행은 건드리지 않아도 됨)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;⑥ 변동금리 + 만기 일시상환&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;매월 이자만 납부하는데 , 금리가 변동될 때마다 납입 이자액이 바뀝니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779006252123&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;변동주기 도래 시:
  1. 새 기준금리 조회
  2. 매월 이자 = 대출원금 &amp;times; 새 월 이자율 로 재계산
  3. 만기 원금 상환은 변동 없음

&amp;rarr; 6가지 중 재계산이 가장 단순
&amp;rarr; 원금은 항상 고정이므로 만기 회차만 별도 관리&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;3&lt;/b&gt;&lt;b&gt;. 연체 이자율 결정&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OuFCy/dJMcafzXDHX/W0fkZnkNsaaOB2LkWnvJKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OuFCy/dJMcafzXDHX/W0fkZnkNsaaOB2LkWnvJKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OuFCy/dJMcafzXDHX/W0fkZnkNsaaOB2LkWnvJKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOuFCy%2FdJMcafzXDHX%2FW0fkZnkNsaaOB2LkWnvJKK%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;1286&quot; height=&quot;260&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;약관해석&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780206524791&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;연체 이자율 = 대출이자율 + 연체가산이자율(3%) 
상한선 : 최고 연 15% (단 , 대출 이자율이 이미 15% 이상이면 대출 이자율 + 2%)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아이디어&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780206524791&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;연체가산이자율 = 0.03  # 연 3% 고정

if 대출이자율 &amp;gt;= 0.15:
    연체이자율 = 대출이자율 + 0.02
else:
    연체이자율 = min(대출이자율 + 연체가산이자율, 0.15)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 연체금 결정&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8nzeg/dJMb997yUsv/d5d2nwrIDkKDdlX71i6jP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8nzeg/dJMb997yUsv/d5d2nwrIDkKDdlX71i6jP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8nzeg/dJMb997yUsv/d5d2nwrIDkKDdlX71i6jP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8nzeg%2FdJMb997yUsv%2Fd5d2nwrIDkKDdlX71i6jP1%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;1284&quot; height=&quot;1074&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;약관해석&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;1,2,3,6 항을 정리하면 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&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;1450&quot; data-origin-height=&quot;1156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjfRM8/dJMcaipT7Wi/bKkW82sgpi9bR68NnfkZyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjfRM8/dJMcaipT7Wi/bKkW82sgpi9bR68NnfkZyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjfRM8/dJMcaipT7Wi/bKkW82sgpi9bR68NnfkZyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjfRM8%2FdJMcaipT7Wi%2FbKkW82sgpi9bR68NnfkZyk%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;1450&quot; height=&quot;1156&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;1156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;연체금의 결정은 상환방법 , 연체금 종류 , 연속 연체 횟수 , 경과 기간에 따라 분기 되어 결정 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;빨간 박스에 해당하는 경우 , 기한 이익 상실이 발동 되어 원금 잔액 전체가 연체 이자 계산의 기준이 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;단 , 계약시 대출 원금이 5천만원 미만 이라면 , 기한 이익 상실이 되어도 원금 잔액에 연체 이자를 부과 하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아이디어&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;약관의 플로우를 코드로 옮기면 아래와 같은 형태가 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780206524792&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;if 상환방법 == 만기일시상환:
    if 연체금 종류 == 이자:
        if 경과기간 &amp;lt; 1개월:
            연체이자 = 미납이자 &amp;times; 연체이자율
        else:  # 1개월 이상 경과 &amp;rarr; 기한이익 상실
            연체이자 = 원금잔액 &amp;times; 연체이자율  (5천만원 미만 예외 제외)
    else:  # 원금 연체
        연체이자 = 원금잔액 &amp;times; 연체이자율

elif 상환방법 in [원금균등, 원리금균등]:
    if 연속연체횟수 == 1:
        연체이자 = 분할상환금 &amp;times; 연체이자율
    else:  # 2회 이상 &amp;rarr; 기한이익 상실
        연체이자 = 원금잔액 &amp;times; 연체이자율  (5천만원 미만 예외 제외)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(1) 상환 스케줄 - 금액 항목 분리 저장&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;상환 스케줄에서 매달 갚아야 하는 금액을 단일 컬럼으로 저장하면, 연체가 발생했을 때 어떤 항목이 미납인지 특정할 수 없습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;약관에서 연체이자 부과 기준이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;미납이자&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;원금잔액&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;분할상환금&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;세 가지로 나뉘는 만큼, 상환 스케줄도 항목별로 분리해서 저장해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;분리해야 &quot;이자는 납부했지만 원금은 미납&quot;이거나, &quot;연체이자 일부만 납부&quot;한 상황을 정확히 추적할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(2) 연체이자 부과 기준 - 3가지 케이스 분리&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&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;1272&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HK1cp/dJMcadvhKgV/XasAn0cXRdyTQutK3xB9W1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HK1cp/dJMcadvhKgV/XasAn0cXRdyTQutK3xB9W1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HK1cp/dJMcadvhKgV/XasAn0cXRdyTQutK3xB9W1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHK1cp%2FdJMcadvhKgV%2FXasAn0cXRdyTQutK3xB9W1%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;1272&quot; height=&quot;334&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;세 케이스는 계산 기준이 완전히 다르기 때문에 하나의 로직으로 처리하면 분기가 뒤엉킵니다. 케이스별로 계산 책임을 분리해두는 것이 좋습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(3) 상환 스케줄 상태 &amp;rarr; 대출 계좌 상태 전이 &amp;rarr; 연체 이자 계산&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;연체 이자를 계산할 때 , 매번 연체금 종류 , 경과 기간 , 연속 연체 횟수를 판단 할 수는 없습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;대신 상환 스케줄의 상태가 대출 계좌의 상태를 바꾸고, 연체 이자 계산은 대출 계좌 상태만 보고 동작하도록 흐름을 분리하는 것이 자연스럽습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780206524793&quot; class=&quot;awk&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;상환 스케줄 상태 변화 (정상 납부 / 1개월 미납 / ...) 
  &amp;rarr; 대출 계좌 상태 전이 (NORMAL / OVERDUE / ...)
  &amp;rarr; 계좌 상태 기준으로 연체이자 부과 케이스 결정&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 채무변제충당순서&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biM9yA/dJMcabK8S5c/kimbHVInvegEssKAnvoAAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biM9yA/dJMcabK8S5c/kimbHVInvegEssKAnvoAAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biM9yA/dJMcabK8S5c/kimbHVInvegEssKAnvoAAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiM9yA%2FdJMcabK8S5c%2FkimbHVInvegEssKAnvoAAk%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;1272&quot; height=&quot;266&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;약관해석&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;채무변제충당순서란 ?&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;대출 상환 시, 납부한 돈을 어느 항목에 먼저 적용할지의 순서 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;고객이 그달에 갚아야 하는 돈을 전부 갚지 못하고 일부만 갚을 경우 , 어느 항목에 대한 돈 부터 먼저 충당 되는지 순서를 나타냅니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;대출 상환 시 발생하는 항목에는 3가지가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비용 : 연체 관련 비용&lt;/li&gt;
&lt;li&gt;이자 : 약정 이자&lt;/li&gt;
&lt;li&gt;원금 : 실제 빌린 돈&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;882&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qGzCj/dJMcab5nOYl/KK4u8tPlCUUxMYHXVND5z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qGzCj/dJMcab5nOYl/KK4u8tPlCUUxMYHXVND5z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qGzCj/dJMcab5nOYl/KK4u8tPlCUUxMYHXVND5z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqGzCj%2FdJMcab5nOYl%2FKK4u8tPlCUUxMYHXVND5z0%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;882&quot; height=&quot;328&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;이러한 채무변제충당순서는 기한이익이 상실된 대출(대출 잔여 원금에 대해 연체이자가 부과되는 상태)에 한하여 변경을 신청할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아이디어&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(1) 상환 시 차감 순서를 코드로 표현&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;납입금이 들어왔을 때, 비용 -&amp;gt; 이자 -&amp;gt; 원금 혹은 비용 -&amp;gt; 원금 -&amp;gt; 이자 순으로 잔액을 차감하는 로직이 필요 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(2) 대출 상태 구분&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;정상 상태 , 단순 연체 상태 , 기한 이익 상실 상태를 구분하여 대출 계좌의 상태를 관리해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;기한이익상실 상태일 때만 충당 순서 변경을 허용하는 조건이 필요합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(3) 충당 순서 변경 처리&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;순서가 바뀌면 같은 납입금이라도 원금이 먼저 줄어드는 방식으로 동작해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;두 가지 충당 방식을 전환할 수 있는 구조가 필요합니다.&lt;/p&gt;</description>
      <category>프로젝트/대출금 상환 프로젝트</category>
      <author>watanabe3</author>
      <guid isPermaLink="true">https://loan-project.tistory.com/25</guid>
      <comments>https://loan-project.tistory.com/25#entry25comment</comments>
      <pubDate>Sun, 17 May 2026 14:11:04 +0900</pubDate>
    </item>
    <item>
      <title>기본 데이터 처리 - DISTINCT</title>
      <link>https://loan-project.tistory.com/24</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DISTINCT 처리 방식 분석 - MySQL 옵티마이저는 무엇을 다르게 하는가 ?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SQL을 작성할 때, DISTINCT를 아무 생각 없이 붙이기 쉽습니다. 하지만 MySQL 내부에서는 같은 DISTINCT라도 처리하는 방식이 완전히 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;DISTINCT의 처리 방식은 옵티마이저가 실행 계획을 수립하는 시점에 결정됩니다. 그리고 옵티마이저는 상황에 따라 전혀 다른 방식으로 중복 제거를 처리합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 글에서는 MySQL이 DISTINCT를 처리하는 방식을 세 가지로 나눠 다뤄보겠습니다. 주요 내용은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;집합 함수가 없는 경우의 DISTINCT&amp;nbsp;&lt;/li&gt;
&lt;li&gt;집합 함수 + DISTINCT, 인덱스 X&amp;nbsp;&lt;/li&gt;
&lt;li&gt;집합 함수 + DISTINCT, 인덱스 O&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;예시 테이블 , 데이터 , 인덱스&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ERD]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;219&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPRBxL/dJMcaakVZZL/OT9MXGZteEA0qFCcVtKJo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPRBxL/dJMcaakVZZL/OT9MXGZteEA0qFCcVtKJo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPRBxL/dJMcaakVZZL/OT9MXGZteEA0qFCcVtKJo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPRBxL%2FdJMcaakVZZL%2FOT9MXGZteEA0qFCcVtKJo1%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;899&quot; height=&quot;219&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;219&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[인덱스 구성]&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778462341125&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;customers
  └─ (인덱스 없음)                               &amp;larr; Case 1 인덱스 미사용 재현용

loans
  └─ idx_loan_type          (loan_type)          &amp;larr; Case 1, 2 기본 스캔용
  └─ idx_type_amount        (loan_type, amount)  &amp;larr; Case 3 루스 인덱스 스캔용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[CREATE 쿼리]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778460612887&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 고객 테이블
CREATE TABLE customers (
  customer_id   INT          NOT NULL AUTO_INCREMENT,
  customer_name VARCHAR(100) NOT NULL,
  region        VARCHAR(50)  NOT NULL,
  credit_grade  CHAR(1)      NOT NULL,  -- A, B, C, D
  PRIMARY KEY (customer_id)
);

-- 대출 테이블
CREATE TABLE loans (
  loan_id       INT           NOT NULL AUTO_INCREMENT,
  customer_id   INT           NOT NULL,
  loan_type     VARCHAR(50)   NOT NULL,  -- '주택담보', '신용', '자동차'
  loan_amount   DECIMAL(15,2) NOT NULL,
  loan_status   VARCHAR(20)   NOT NULL,  -- '정상', '연체', '상환완료'
  created_at    DATE          NOT NULL,
  PRIMARY KEY (loan_id),
  KEY idx_customer (customer_id),
  KEY idx_loan_type_amount (loan_type, loan_amount)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[샘플 데이터 삽입]&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778462224575&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INSERT INTO customers (customer_name, region, credit_grade) VALUES
  ('김민준', '서울', 'A'),
  ('이서연', '부산', 'B'),
  ('박지훈', '서울', 'A'),
  ('최수아', '대구', 'C'),
  ('정우성', '서울', 'B'),
  ('한지민', '부산', 'A'),
  ('오세훈', '대구', 'C'),
  ('윤아라', '서울', 'D');

INSERT INTO loans (customer_id, loan_type, loan_amount, loan_status, created_at) VALUES
  (1, '주택담보', 150000000, '정상',    '2023-01-15'),
  (1, '신용',    5000000,   '정상',    '2023-06-01'),
  (2, '자동차',  30000000,  '연체',    '2022-11-20'),
  (2, '주택담보', 200000000, '정상',    '2021-05-10'),
  (3, '신용',    8000000,   '상환완료', '2020-03-22'),
  (3, '신용',    12000000,  '정상',    '2023-09-01'),
  (4, '자동차',  25000000,  '정상',    '2023-02-14'),
  (5, '주택담보', 180000000, '연체',    '2022-07-30'),
  (5, '신용',    6000000,   '상환완료', '2021-12-05'),
  (6, '자동차',  35000000,  '정상',    '2023-04-18'),
  (7, '신용',    9000000,   '연체',    '2022-08-11'),
  (8, '주택담보', 120000000, '정상',    '2023-10-01'),
  (8, '신용',    7000000,   '상환완료', '2021-06-15'),
  (8, '자동차',  28000000,  '정상',    '2023-03-09');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;CASE 1 : 집합 함수가 없는 경우의 DISTINCT&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[DISTINCT = GROUP BY]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;집합함수 없이 사용된 DISTINCT 는 MySQL 내부에서 GROUP BY 와 동일하게 처리 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 , 아래 두 쿼리는 MySQL 내부에서 동일하게 처리 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778463062507&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 아래 두 쿼리는 MySQL 내부에서 동일하게 처리된다
SELECT DISTINCT loan_type FROM loans;
SELECT loan_type FROM loans GROUP BY loan_type;&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;size18&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;1018&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWO74Y/dJMcac36HnC/8sztUm4WJM3CX6tstFxrh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWO74Y/dJMcac36HnC/8sztUm4WJM3CX6tstFxrh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWO74Y/dJMcac36HnC/8sztUm4WJM3CX6tstFxrh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWO74Y%2FdJMcac36HnC%2F8sztUm4WJM3CX6tstFxrh0%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;1018&quot; height=&quot;267&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[DISTINCT는 SELECT 전체에 적용된다]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;집합함수 없이 사용된 DISTINCT 는 특정 컬럼 하나에만 적용되는 것이 아니라 , SELECT 절 전체 컬럼 조합에 적용 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 , 아래 두 쿼리는 동일하게 동작 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778463366716&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 아래 두 쿼리는 같은 쿼리
SELECT DISTINCT(loan_type), loan_status FROM loans;
SELECT DISTINCT loan_type, loan_status FROM loans;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;DISTINCT 는 함수가 아니기에 괄호는 무시 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;CASE 2 : 집합 함수 + DISTINCT , 인덱스 X &lt;/b&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[집합 함수란?]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집합 함수는 여러 행의 값을 하나의 값으로 집계하는 함수입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COUNT() , SUM() , AVG() , MIN() , MAX() 가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 집합 함수 안에 DISTINCT 를 함께 사용할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778466667458&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 대출 유형이 몇 종류인지 (중복 제거 후 카운트)
SELECT COUNT(DISTINCT loan_type) FROM loans;

-- 대출 유형별 중복 제거된 최소/최대 대출금액
SELECT MIN(DISTINCT loan_amount), MAX(DISTINCT loan_amount) FROM loans;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[어떻게 처리되는가]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778463763899&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT COUNT(DISTINCT loan_status) FROM loans;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loan_status 컬럼에는 인덱스가 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 은 이 상황에서 아래 순서로 처리합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;loans 테이블을 풀 스캔 (ALL)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;loan_status 값을 임시 테이블에 저장하면서 중복 제거&amp;nbsp;&lt;/li&gt;
&lt;li&gt;임시 테이블의 행 수를 COUNT&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;임시 테이블이 생성&lt;/b&gt; 된다는 점 입니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시 테이블은 유니크 키를 가지고 있어서 , 값을 삽입할 때 중복이 자동으로 걸러집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 많아질수록 이 임시 테이블 생성 비용이 쿼리 성능에 직접적인 영향을 줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[실행 계획 분석]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPxdMv/dJMcafGzwpy/ljT8hrYkzBTxvNvdooPvZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPxdMv/dJMcafGzwpy/ljT8hrYkzBTxvNvdooPvZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPxdMv/dJMcafGzwpy/ljT8hrYkzBTxvNvdooPvZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPxdMv%2FdJMcafGzwpy%2FljT8hrYkzBTxvNvdooPvZ0%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;815&quot; height=&quot;132&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획은 위와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;type : ALL 은 풀 스캔을 의미 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 인덱스가 존재하지 않아 loans 테이블을 풀 스캔하고 있음을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 임시 테이블을 사용하면 Extra 컬럼에 &quot;Using temporary&quot; 가 표시 되지만 , 해당 경우에서는 표시가 되지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;CASE 3 : 집합 함수 + DISTINCT , 인덱스 O &lt;/b&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[어떻게 처리되는가]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778465895877&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT loan_type, MIN(DISTINCT loan_amount), MAX(DISTINCT loan_amount)
FROM loans
GROUP BY loan_type;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 idx_type_amount (loan_type , loan_amount) 인덱스를 활용할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 loan_type 순으로 정렬되어 있고 , 같은 loan_type 안에서는 loan_amount 순으로 정렬되어 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 은 이 인덱스를 순서대로 읽는 것만으로 아래 두 가지를 동시에 처리 합니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GROUP BY loan_type &amp;mdash; 인덱스가 이미 loan_type 순으로 정렬되어 있으므로 별도 정렬 불필요&lt;/li&gt;
&lt;li&gt;MIN(), MAX() &amp;mdash; 각 그룹의 첫 번째 값이 MIN, 마지막 값이 MAX이므로 임시 테이블 불필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 인덱스를 처음부터 끝까지 순서대로 읽는 것만으로 집계가 완료됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[실행 계획 분석]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1029&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO2U67/dJMcafT6CeH/F5r6TRAdRO4KyBktElk2zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO2U67/dJMcafT6CeH/F5r6TRAdRO4KyBktElk2zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO2U67/dJMcafT6CeH/F5r6TRAdRO4KyBktElk2zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO2U67%2FdJMcafT6CeH%2FF5r6TRAdRO4KyBktElk2zK%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;1029&quot; height=&quot;164&quot; data-origin-width=&quot;1029&quot; data-origin-height=&quot;164&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;type: index - 테이블이 아닌 인덱스를 풀 스캔 했음을 의미 합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;key: idx_loan_type_amount - loan_type_amount 인덱스가 실제로 사용됐음을 의미합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Extra: 테이블을 전혀 건드리지 않고 인덱스만으로 쿼리가 완료됐음을 의미합니다. (커버링 인덱스)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;인덱스 추가로 쿼리 개선하기(CASE 2 를 CASE 3 로)&amp;nbsp; &lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[개선 전 - 인덱스 없이 집계]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778466927906&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT loan_status,
       COUNT(DISTINCT loan_type)
FROM loans
GROUP BY loan_status;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출 상태 별로 대출 유형이 몇 종류 인지 집계 하는 쿼리 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loan_status 컬럼에는 인덱스가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 확인해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;877&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUwAnS/dJMcafzNRJu/skc2knRGBjJYLYuExWlk10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUwAnS/dJMcafzNRJu/skc2knRGBjJYLYuExWlk10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUwAnS/dJMcafzNRJu/skc2knRGBjJYLYuExWlk10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUwAnS%2FdJMcafzNRJu%2Fskc2knRGBjJYLYuExWlk10%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;877&quot; height=&quot;160&quot; data-origin-width=&quot;877&quot; data-origin-height=&quot;160&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;type : ALL 로 풀 스캔이 발생&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Extra : Using temporary 가 나타나진 않지만 , 집계 함수와 DISTINCT 가 함께 사용되었기에 임시 테이블 사용&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;지금은 데이터가 14건 뿐이라 체감하기 어렵지만 , 실제 운영 환경처럼 데이터가 수백만 건으로 늘어나면 어떻게 될까요 ?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 확인해보겠습니다. 아래 프로시저로 100만 건의 데이터를 삽입 합니다. (2분 가량 걸림)&lt;/p&gt;
&lt;pre id=&quot;code_1778467618537&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DELIMITER $$

CREATE PROCEDURE insert_loan_data()
BEGIN
  DECLARE i INT DEFAULT 1;
  DECLARE v_customer_id INT;
  DECLARE v_loan_type VARCHAR(50);
  DECLARE v_loan_amount DECIMAL(15,2);
  DECLARE v_loan_status VARCHAR(20);
  DECLARE v_created_at DATE;

  WHILE i &amp;lt;= 1000000 DO
    SET v_customer_id = FLOOR(1 + RAND() * 8);
    SET v_loan_type = ELT(FLOOR(1 + RAND() * 3), '주택담보', '신용', '자동차');
    SET v_loan_amount = FLOOR(1000000 + RAND() * 500000000);
    SET v_loan_status = ELT(FLOOR(1 + RAND() * 3), '정상', '연체', '상환완료');
    SET v_created_at = DATE_ADD('2020-01-01', INTERVAL FLOOR(RAND() * 1460) DAY);

    INSERT INTO loans (customer_id, loan_type, loan_amount, loan_status, created_at)
    VALUES (v_customer_id, v_loan_type, v_loan_amount, v_loan_status, v_created_at);

    SET i = i + 1;
  END WHILE;
END$$

DELIMITER ;

CALL insert_loan_data();&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;</description>
      <category>기술 파보기/MySQL</category>
      <author>watanabe3</author>
      <guid isPermaLink="true">https://loan-project.tistory.com/24</guid>
      <comments>https://loan-project.tistory.com/24#entry24comment</comments>
      <pubDate>Mon, 11 May 2026 11:25:40 +0900</pubDate>
    </item>
    <item>
      <title>기본 데이터 처리 - GROUP BY</title>
      <link>https://loan-project.tistory.com/23</link>
      <description>&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;GROUP BY, 다 같은 집계가 아닙니다.&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;div id=&quot;article-view&quot; style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SQL을 작성할 때, GROUP BY를 아무 생각 없이 붙이기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 MySQL 내부에서는 같은 GROUP BY라도 처리하는 방식이 완전히 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GROUP BY의 처리 방식은 옵티마이저가 실행 계획을 수립하는 시점에 결정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 옵티마이저는 상황에 따라 전혀 다른 방식으로 집계를 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;똑같이 GROUP BY를 작성했더라도, 인덱스를 활용해 집계를 효율적으로 처리하기도 하고, 별도의 임시 테이블을 생성해 처리하기도 합니다. 이 차이가 쿼리 성능에 큰 영향을 미칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 글에서는 MySQL이 GROUP BY를 처리하는 방식에 대해 다뤄보겠습니다. 주요 내용은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 차례대로 읽는 &lt;b&gt;타이트 인덱스 스캔 (Tight Index Scan)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;인덱스를 건너뛰며 읽는 &lt;b&gt;루스 인덱스 스캔 (Loose Index Scan)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;인덱스를 사용하지 못할 때의 &lt;b&gt;임시 테이블 (Temporary Table)&lt;/b&gt;&amp;nbsp;방식&lt;/li&gt;
&lt;li&gt;실제로 어떤 방식이 선택됐는지 &lt;b&gt;실행 계획으로 확인하는 방법&lt;/b&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;예시 테이블 , 데이터 , 인덱스(수정 필요)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ERD]&lt;/b&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1784&quot; data-origin-height=&quot;1148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rDurj/dJMcahqO6XP/GpeMWra2Q5O1Nw0ajuETNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rDurj/dJMcahqO6XP/GpeMWra2Q5O1Nw0ajuETNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rDurj/dJMcahqO6XP/GpeMWra2Q5O1Nw0ajuETNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrDurj%2FdJMcahqO6XP%2FGpeMWra2Q5O1Nw0ajuETNK%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;1784&quot; height=&quot;1148&quot; data-origin-width=&quot;1784&quot; data-origin-height=&quot;1148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[인덱스 구성]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778203179120&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;customers
  └─ idx_region_age       (region, age)                  &amp;larr; 타이트 인덱스 스캔 실습용

loans
  └─ idx_type_status      (loan_type, status)            &amp;larr; 타이트 인덱스 스캔 실습용
  └─ idx_branch_type_status (branch_id, loan_type, status) &amp;larr; 루스 인덱스 스캔 실습용

repayments
  └─ idx_loan_date        (loan_id, paid_date)           &amp;larr; 타이트 인덱스 스캔 실습용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[CREATE 쿼리]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778203218803&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 고객 테이블
CREATE TABLE customers (
    customer_id  INT          NOT NULL AUTO_INCREMENT,
    name         VARCHAR(100) NOT NULL,
    region       VARCHAR(50)  NOT NULL,
    age          INT          NOT NULL,
    credit_score INT          NOT NULL,
    join_date    DATE         NOT NULL,
    PRIMARY KEY (customer_id),
    INDEX idx_region_age (region, age)
);

-- 대출 테이블
CREATE TABLE loans (
    loan_id       INT            NOT NULL AUTO_INCREMENT,
    customer_id   INT            NOT NULL,
    loan_type     VARCHAR(30)    NOT NULL,  -- 'mortgage','car','personal','business'
    status        VARCHAR(20)    NOT NULL,  -- 'active','closed','overdue'
    branch_id     INT            NOT NULL,
    loan_amount   DECIMAL(15,2)  NOT NULL,
    interest_rate DECIMAL(5,2)   NOT NULL,
    start_date    DATE           NOT NULL,
    end_date      DATE           NOT NULL,
    PRIMARY KEY (loan_id),
    INDEX idx_type_status           (loan_type, status),
    INDEX idx_branch_type_status    (branch_id, loan_type, status),
    INDEX idx_customer              (customer_id)
);

-- 상환 내역 테이블
CREATE TABLE repayments (
    repayment_id   INT           NOT NULL AUTO_INCREMENT,
    loan_id        INT           NOT NULL,
    paid_date      DATE          NOT NULL,
    amount         DECIMAL(15,2) NOT NULL,
    payment_method VARCHAR(30)   NOT NULL,  -- 'transfer','card','cash'
    PRIMARY KEY (repayment_id),
    INDEX idx_loan_date (loan_id, paid_date)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[샘플 데이터 삽입]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778203254537&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INSERT INTO customers (name, region, age, credit_score, join_date) VALUES
('김민준', '서울', 35, 820, '2019-03-15'),
('이서연', '경기', 42, 750, '2018-07-22'),
('박도윤', '부산', 28, 690, '2021-01-10'),
('최지우', '서울', 55, 880, '2015-11-05'),
('정하은', '대구', 31, 710, '2020-06-18'),
('강지호', '경기', 47, 800, '2017-09-30'),
('윤서준', '인천', 39, 760, '2019-12-01'),
('임나윤', '서울', 26, 650, '2022-04-25');

INSERT INTO loans
  (customer_id, loan_type, status, branch_id, loan_amount, interest_rate, start_date, end_date)
VALUES
(1, 'mortgage', 'active',  1, 250000000, 3.50, '2021-03-01', '2041-03-01'),
(1, 'car',      'closed',  1,  15000000, 5.20, '2019-05-01', '2024-05-01'),
(2, 'personal', 'active',  2,   5000000, 7.80, '2023-01-15', '2025-01-15'),
(2, 'mortgage', 'active',  2, 180000000, 3.20, '2020-08-01', '2040-08-01'),
(3, 'car',      'overdue', 1,  20000000, 6.00, '2022-06-01', '2027-06-01'),
(4, 'business', 'active',  3, 100000000, 4.50, '2021-11-01', '2031-11-01'),
(4, 'personal', 'closed',  3,   8000000, 8.50, '2018-03-01', '2021-03-01'),
(5, 'mortgage', 'active',  2, 210000000, 3.80, '2022-02-01', '2042-02-01'),
(6, 'car',      'active',  1,  25000000, 5.50, '2023-07-01', '2028-07-01'),
(7, 'personal', 'overdue', 3,   3000000, 9.00, '2023-03-01', '2025-03-01'),
(8, 'business', 'active',  2,  50000000, 5.00, '2024-01-01', '2029-01-01');

INSERT INTO repayments (loan_id, paid_date, amount, payment_method) VALUES
(1,  '2024-01-05',  800000, 'transfer'),
(1,  '2024-02-05',  800000, 'transfer'),
(1,  '2024-03-05',  800000, 'transfer'),
(2,  '2024-01-10',  300000, 'card'),
(3,  '2024-02-20',  150000, 'transfer'),
(4,  '2024-01-08',  600000, 'transfer'),
(4,  '2024-02-08',  600000, 'transfer'),
(5,  '2024-01-15',  400000, 'cash'),
(6,  '2024-03-01', 1200000, 'transfer'),
(8,  '2024-01-20',  500000, 'card'),
(9,  '2024-02-25',  450000, 'transfer'),
(11, '2024-03-10',  700000, 'transfer');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/div&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;타이트 인덱스 스캔&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[개념]&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;타이트 인덱스 스캔은 GROUP BY 를 처리할 때 인덱스를 처음부터 끝까지 순서대로 읽는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이름 그대로 인덱스를 빠짐없이(tight 하게) 읽습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;별도의 정렬 작업이나 임시 테이블 없이, 인덱스가 이미 정렬된 상태로 저장되어 있다는 점을 그대로 활용 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인덱스를 순서대로 읽으면서 값이 바뀌는 경계 지점마다 집계를 수행하면, GROUP BY 결과를 자연스럽게 만들어 낼 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[동작방식]&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778203601906&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;idx_type_status 인덱스 (loan_type, status) 저장 구조

┌──────────────┬──────────┬─────────┐
│   loan_type  │  status  │ loan_id │
├──────────────┼──────────┼─────────┤
│ business     │ active   │  6      │
│ business     │ active   │  11     │
│ car          │ active   │  9      │  &amp;larr; (car, active) 그룹 시작
│ car          │ closed   │  2      │  &amp;larr; 값 바뀜 &amp;rarr; 이전 그룹 집계
│ car          │ overdue  │  5      │  &amp;larr; 값 바뀜 &amp;rarr; 이전 그룹 집계
│ mortgage     │ active   │  1      │
│ mortgage     │ active   │  4      │
│ mortgage     │ active   │  8      │
│ personal     │ active   │  3      │
│ personal     │ closed   │  7      │
│ personal     │ overdue  │  10     │
└──────────────┴──────────┴─────────┘

&amp;rarr; 인덱스를 위에서 아래로 순서대로 읽으면서
  (loan_type, status) 조합이 바뀌는 지점마다 집계
&amp;rarr; 별도 정렬 없음, 임시 테이블 없음&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;size18&quot;&gt;&lt;b&gt;[예시쿼리]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Case 1. GROUP BY 컬럼이 인덱스와 완전히 일치하는 경우&lt;/p&gt;
&lt;pre id=&quot;code_1778203903322&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- idx_type_status (loan_type, status) 인덱스 활용
SELECT loan_type,
       status,
       COUNT(*)         AS loan_count,
       AVG(loan_amount) AS avg_amount
FROM loans
GROUP BY loan_type, status;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Case 2. WHERE 로 선행 컬럼을 고정하고 나머지로 GROUP BY 하는 경우&lt;/p&gt;
&lt;pre id=&quot;code_1778204003855&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- idx_branch_type_status (branch_id, loan_type, status) 인덱스 활용
SELECT loan_type,
       status,
       COUNT(*)         AS loan_count,
       SUM(loan_amount) AS total_amount
FROM loans
WHERE branch_id = 1
GROUP BY loan_type, status;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Case 3. 인덱스의 앞부분 컬럼만으로 GROUP BY 하는 경우&lt;/p&gt;
&lt;pre id=&quot;code_1778204051072&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- idx_type_status (loan_type, status) 인덱스 활용
SELECT loan_type,
       COUNT(*)          AS loan_count,
       MAX(loan_amount)  AS max_amount
FROM loans
GROUP BY loan_type;&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;size18&quot;&gt;&lt;b&gt;[실행 계획]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778204101088&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN
SELECT loan_type,
       status,
       COUNT(*),
       AVG(loan_amount)
FROM loans
GROUP BY loan_type, status;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&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;1061&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnMpBV/dJMcaad91TF/vHBjVBvMBrzW6QylPKwH51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnMpBV/dJMcaad91TF/vHBjVBvMBrzW6QylPKwH51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnMpBV/dJMcaad91TF/vHBjVBvMBrzW6QylPKwH51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnMpBV%2FdJMcaad91TF%2FvHBjVBvMBrzW6QylPKwH51%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;1061&quot; height=&quot;106&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778204218622&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type  &amp;rarr; index
       인덱스 전체를 순서대로 스캔한다는 의미입니다.
       테이블을 직접 읽지 않습니다.

key   &amp;rarr; idx_type_status
       실제로 사용된 인덱스입니다.

Extra &amp;rarr; NULL
       Using temporary 없음
       Using filesort 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;루스 인덱스 스캔&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[개념]&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;루스 인덱스 스캔은 인덱스를 처음부터 끝까지 전부 읽지 않고, 각 그룹에서 필요한 레코드만 골라 듬성듬성 읽는 방식 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;타이트 인덱스 스캔과의 차이를 한 문장으로 표현하면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778206995287&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;타이트 인덱스 스캔 &amp;rarr; 인덱스를 처음부터 끝까지 전부 읽는다
루스 인덱스 스캔  &amp;rarr; 각 그룹의 첫 번째 또는 마지막 레코드만 읽고 나머지는 건너뛴다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;루스 인덱스 스캔이 레코드를 건너뛸 수 있는 이유는, &lt;b&gt;인덱스가 이미 정렬된 상태로 저장되어 있기 때문&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예를들면 , MIN() / MAX() 를 구할 때, 어차피 그룹 내 첫 번째 레코드가 MIN, 마지막 레코드가 MAX라는 것이 인덱스 정렬로 이미 보장됩니다. 중간 레코드를 전부 읽을 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단, 이것이 가능하려면 &lt;b&gt;MIN/MAX 대상 컬럼이 인덱스에 포함되어 있어야&lt;/b&gt; 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인덱스에 없는 컬럼의 MIN/MAX는 결국 테이블을 전부 읽어야 하므로 루스 인덱스 스캔의 이점을 살릴 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[동작방식]&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래 쿼리를 기준으로 동작 방식을 설명하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1778207122720&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN
SELECT branch_id,
       loan_type,
       MIN(interest_rate) AS min_rate,
       MAX(interest_rate) AS max_rate
FROM loans
GROUP BY branch_id, loan_type;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;idx_branch_type_rate 인덱스에서 interest_rate 가 인덱스에 포함되어 있고, 이미 오름차순으로 정렬되어 있으므로 각 그룹에서 첫번째 레코드 = MIN , 마지막 레코드 = MAX 가 보장됩니다. 중간 레코드를 전부 읽을 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778207305203&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;STEP 1. 인덱스의 첫 번째 레코드로 점프 후 그룹별 순회

┌───────────┬──────────────┬───────────────┬─────────┐
│ branch_id │  loan_type   │ interest_rate │ loan_id │
├───────────┼──────────────┼───────────────┼─────────┤
│     1     │ car          │     5.20      │  2      │ &amp;larr; ✅ 읽음 (branch=1, car 그룹의 MIN)
│     1     │ car          │     5.30      │  14     │ &amp;larr; ⏭ SKIP
│     1     │ car          │     5.50      │  9      │ &amp;larr; ⏭ SKIP
│     1     │ car          │     5.80      │  13     │ &amp;larr; ⏭ SKIP
│     1     │ car          │     6.00      │  5      │ &amp;larr; ⏭ SKIP
│     1     │ car          │     6.10      │  15     │ &amp;larr; ✅ 읽음 (branch=1, car 그룹의 MAX)
│     1     │ mortgage     │     3.10      │  16     │ &amp;larr; ✅ 읽음 (branch=1, mortgage 그룹의 MIN)
│     1     │ mortgage     │     3.50      │  1      │ &amp;larr; ⏭ SKIP
│     1     │ mortgage     │     3.90      │  17     │ &amp;larr; ✅ 읽음 (branch=1, mortgage 그룹의 MAX)
│     2     │ business     │     5.00      │  11     │ &amp;larr; ✅ 읽음 (branch=2, business 그룹의 MIN)
│     2     │ business     │     5.00      │  11     │ &amp;larr; ✅ 읽음 (branch=2, business 그룹의 MAX)
│     2     │ mortgage     │     3.20      │  4      │ &amp;larr; ✅ 읽음 (branch=2, mortgage 그룹의 MIN)
│     2     │ mortgage     │     3.80      │  8      │ &amp;larr; ⏭ SKIP
│     2     │ mortgage     │     7.80      │  3      │ &amp;larr; ✅ 읽음 (branch=2, mortgage 그룹의 MAX)
│     2     │ personal     │     7.80      │  3      │ &amp;larr; ✅ 읽음 (branch=2, personal 그룹의 MIN/MAX)
│     3     │ business     │     4.50      │  6      │ &amp;larr; ✅ 읽음 (branch=3, business 그룹의 MIN/MAX)
│     3     │ personal     │     8.50      │  7      │ &amp;larr; ⏭ SKIP
│     3     │ personal     │     9.00      │  10     │ &amp;larr; ✅ 읽음 (branch=3, personal 그룹의 MAX)
└───────────┴──────────────┴───────────────┴─────────┘

&amp;rarr; 전체 16건 중 실제로 읽은 레코드는 일부에 불과
&amp;rarr; (branch_id, loan_type) 그룹이 바뀔 때마다 첫/마지막 레코드로 점프
&amp;rarr; 중간 레코드는 건너뜀


STEP 2. 집계 결과 반환

  branch_id │ loan_type │ min_rate │ max_rate
  ──────────┼───────────┼──────────┼─────────
      1     │ car       │   5.20   │   6.10
      1     │ mortgage  │   3.10   │   3.90
      2     │ business  │   5.00   │   5.00
      2     │ mortgage  │   3.20   │   7.80
      2     │ personal  │   7.80   │   7.80
      3     │ business  │   4.50   │   4.50
      3     │ personal  │   8.50   │   9.00&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;7 개의 레코드를 건너 뛰었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-size: 1.12em; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;interest_rate가 인덱스에 정렬된 상태로 저장되어 있기 때문에 중간 레코드를 볼 필요가 없습니다. 데이터가 많아질수록, 그룹 내 레코드가 많아질수록 건너뛰는 양이 극적으로 늘어납니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[예시쿼리]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;Case 1. 인덱스 선두 컬럼만으로 GROUP BY + MIN/MAX&lt;/p&gt;
&lt;pre id=&quot;code_1778220705030&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- idx_branch_type_rate (branch_id, loan_type, interest_rate) 인덱스 활용
-- 지점별 최저/최고 금리 조회 (loan_type 무관)
SELECT branch_id,
       MIN(interest_rate) AS min_rate,
       MAX(interest_rate) AS max_rate
FROM loans
GROUP BY branch_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Case 2. MIN 만 사용&lt;/p&gt;
&lt;pre id=&quot;code_1778220774531&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- idx_branch_type_rate (branch_id, loan_type, interest_rate) 인덱스 활용
-- 지점별, 대출 종류별 최저 금리만 조회
SELECT branch_id,
       loan_type,
       MIN(interest_rate) AS min_rate
FROM loans
GROUP BY branch_id, loan_type;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Case 3. COUNT(DISTINCT) 활용&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778220851030&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- idx_branch_type_status (branch_id, loan_type, status) 인덱스 활용
-- 지점별 대출 종류가 몇 가지인지 조회
SELECT branch_id,
       COUNT(DISTINCT loan_type) AS type_count
FROM loans
GROUP BY branch_id;&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;size18&quot;&gt;&lt;b&gt;[실행 계획]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;루스 인덱스 스캔은 옵티마이저가 비용을 계산해 유리하다고 판단할 때만 선택됩니다. 데이터가 적으면 그냥 인덱스를 처음부터 끝까지 읽는 게 더 저렴하다고 판단해 타이트 인덱스 스캔을 선택합니다. 따라서 실행 계획에서 확인하려면 충분한 양의 데이터가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Step 1. 대량의 데이터 삽입&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778218913113&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- branch_id 1~3, loan_type car/mortgage/personal/business 조합으로
-- 그룹 수는 적고 그룹 내 데이터는 많은 구조로 삽입
DELIMITER $$
CREATE PROCEDURE insert_loose_test()
BEGIN
    DECLARE i INT DEFAULT 1;
    DECLARE rate DECIMAL(5,2);
    WHILE i &amp;lt;= 100000 DO
        SET rate = ROUND(3.0 + RAND() * 7.0, 2);
        INSERT INTO loans (customer_id, loan_type, status, branch_id, loan_amount, interest_rate, start_date, end_date)
        VALUES (1, 'car',      'active', 1, 100000000, rate,                    '2020-01-01', '2030-01-01');
        SET rate = ROUND(3.0 + RAND() * 4.0, 2);
        INSERT INTO loans (customer_id, loan_type, status, branch_id, loan_amount, interest_rate, start_date, end_date)
        VALUES (1, 'mortgage', 'active', 1, 200000000, rate,                    '2020-01-01', '2030-01-01');
        SET i = i + 1;
    END WHILE;
END$$
DELIMITER ;

CALL insert_loose_test();
ANALYZE TABLE loans;&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;size18&quot;&gt;루스 인덱스 스캔이 유리한 상황을 만들기 위해 그룹 종류는 적고, 그룹 내 레코드는 많은 구조로 데이터를 삽입합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그룹 내 레코드가 많을수록 첫/마지막 레코드만 읽고 건너뛰는 루스 인덱스 스캔의 이점이 극대화 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Step 2. 실행 계획 확인&lt;/p&gt;
&lt;pre id=&quot;code_1778219014747&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN
SELECT branch_id,
       loan_type,
       MIN(interest_rate) AS min_rate,
       MAX(interest_rate) AS max_rate
FROM loans
GROUP BY branch_id, loan_type;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&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;1266&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m5m1L/dJMb997nI6L/kCi43MfX6OM5CfYkVSGgp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m5m1L/dJMb997nI6L/kCi43MfX6OM5CfYkVSGgp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m5m1L/dJMb997nI6L/kCi43MfX6OM5CfYkVSGgp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm5m1L%2FdJMb997nI6L%2FkCi43MfX6OM5CfYkVSGgp1%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;1266&quot; height=&quot;111&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778219135213&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type  &amp;rarr; range
       인덱스의 특정 범위만 접근한다는 의미입니다.

key   &amp;rarr; idx_branch_type_rate
       실제로 사용된 인덱스입니다.

Extra &amp;rarr; Using index for group-by   ★ 루스 인덱스 스캔의 핵심 시그니처
       이 문구가 보이면 루스 인덱스 스캔이 선택된 것입니다.
       그룹별 첫/마지막 레코드만 읽고 나머지는 건너뛰고 있다는 의미입니다.

       Using temporary 없음  &amp;rarr; 임시 테이블 미사용 ✅
       Using filesort   없음  &amp;rarr; 별도 정렬 미발생  ✅&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;size18&quot;&gt;Step 3. 데이터 원복&lt;/p&gt;
&lt;pre id=&quot;code_1778219193863&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 프로시저 삭제 및 데이터 원복
DROP PROCEDURE IF EXISTS insert_loose_test;
TRUNCATE TABLE loans;

INSERT INTO loans
  (customer_id, loan_type, status, branch_id, loan_amount, interest_rate, start_date, end_date)
VALUES
(1, 'mortgage', 'active',  1, 250000000, 3.50, '2021-03-01', '2041-03-01'),
(1, 'car',      'closed',  1,  15000000, 5.20, '2019-05-01', '2024-05-01'),
(2, 'personal', 'active',  2,   5000000, 7.80, '2023-01-15', '2025-01-15'),
(2, 'mortgage', 'active',  2, 180000000, 3.20, '2020-08-01', '2040-08-01'),
(3, 'car',      'overdue', 1,  20000000, 6.00, '2022-06-01', '2027-06-01'),
(4, 'business', 'active',  3, 100000000, 4.50, '2021-11-01', '2031-11-01'),
(4, 'personal', 'closed',  3,   8000000, 8.50, '2018-03-01', '2021-03-01'),
(5, 'mortgage', 'active',  2, 210000000, 3.80, '2022-02-01', '2042-02-01'),
(6, 'car',      'active',  1,  25000000, 5.50, '2023-07-01', '2028-07-01'),
(7, 'personal', 'overdue', 3,   3000000, 9.00, '2023-03-01', '2025-03-01'),
(8, 'business', 'active',  2,  50000000, 5.00, '2024-01-01', '2029-01-01'),
(1, 'car',      'active',  1,  18000000, 5.80, '2023-01-01', '2028-01-01'),
(2, 'car',      'active',  1,  22000000, 5.30, '2023-06-01', '2028-06-01'),
(3, 'car',      'closed',  1,  12000000, 6.10, '2020-01-01', '2023-01-01'),
(4, 'mortgage', 'active',  1, 300000000, 3.10, '2022-01-01', '2042-01-01'),
(5, 'mortgage', 'closed',  1, 150000000, 3.90, '2015-01-01', '2035-01-01');&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;임시 테이블 사용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[개념]&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞서 살펴본 타이트 인덱스 스캔과 루스 인덱스 스캔은 모두 정렬된 인덱스를 활용해 GROUP BY 를 처리합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 인덱스를 사용할 수 없는 상황이라면 MySQL 은 임시 테이블을 생성해 GROUP BY 를 처리 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[동작방식]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;임시 테이블 방식은 아래와 같이 동작 합니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778221367613&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 테이블 전체를 읽는다 (풀 테이블 스캔 or 인덱스 풀 스캔)
2. GROUP BY 컬럼 기준으로 임시 테이블에 데이터를 쌓는다
3. 임시 테이블을 기반으로 집계를 수행한다
4. 결과를 반환한다 (필요시 정렬까지 수행)&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;size18&quot;&gt;디스크 또는 메모리에 임시 테이블을 만들고 데이터를 전부 집어 넣은 뒤 집계하므로 , 데이터가 많을 수록 성능에 큰 영향을 미칩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[예시 쿼리]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Case 1. GROUP BY 컬럼 순서가 인덱스와 불일치&lt;/p&gt;
&lt;pre id=&quot;code_1778221538362&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- idx_type_status 는 (loan_type, status) 순서인데
-- GROUP BY 순서를 뒤집어서 인덱스 활용 불가
SELECT status,
       loan_type,
       COUNT(*)        AS loan_count,
       SUM(loan_amount) AS total_amount
FROM loans
GROUP BY status, loan_type;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Case 2. GROUP BY 컬럼이 인덱스에 없는 경우&lt;/p&gt;
&lt;pre id=&quot;code_1778221608630&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- credit_score 는 loans 테이블에 없고
-- customers 테이블에 있지만 인덱스 없음
SELECT c.credit_score,
       COUNT(*)         AS loan_count,
       AVG(l.loan_amount) AS avg_amount
FROM loans l
JOIN customers c ON l.customer_id = c.customer_id
GROUP BY c.credit_score;&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;size18&quot;&gt;Case 3. JOIN 이 포함된 GROUP BY&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778221731263&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 조인이 포함되면 단일 테이블 인덱스로 GROUP BY 처리 불가
-- 임시 테이블 생성 후 집계
SELECT c.region,
       l.loan_type,
       COUNT(*)          AS loan_count,
       SUM(l.loan_amount) AS total_amount
FROM loans l
JOIN customers c ON l.customer_id = c.customer_id
GROUP BY c.region, l.loan_type;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[실행 계획]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778221828381&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN
SELECT status,
       loan_type,
       COUNT(*),
       SUM(loan_amount)
FROM loans
GROUP BY status, loan_type;&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;size18&quot;&gt;실행계획에서 확인해야 할 포인트는 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778221868896&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Extra &amp;rarr; Using temporary   ★ 임시 테이블 사용의 핵심 시그니처
       GROUP BY 처리를 위해 내부적으로 임시 테이블을 생성했다는 의미입니다.
       데이터가 많을수록 성능에 직접적인 영향을 미칩니다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GROUP BY 를 작성할 때 MySQL 이 어떤 방식으로 처리할지는 옵티마이저가 결정합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;개발자가 같은 GROUP BY 를 작성하더라도 , 인덱스 구성과 쿼리 형태에 따라 내부 처리 방식은 완전히 달라집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;지금까지 살펴본 3가지 방식을 정리하면 아래와 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 138px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;타이트 인덱스 스캔&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;루스 인덱스 스캔&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;임시 테이블&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;인덱스 사용&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;사용&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;사용&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;사용 or 미사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;읽는 방식&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;인덱스 전체를 순서대로 읽음&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;그룹 첫/마지막 레코드만 점프&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;인덱스 또는 테이블 전체를 읽고 임시 테이블에 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;임시 테이블&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;미생성&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;미생성&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;추가 정렬&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;발생 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;Extra 표시&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;Using index&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;Using index for group-by&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;Using temporary&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;성능&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;좋음&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;매우 좋음 (데이터 많을수록 유리)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;나쁨 (데이터 많을수록 불리)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;</description>
      <category>기술 파보기/MySQL</category>
      <author>watanabe3</author>
      <guid isPermaLink="true">https://loan-project.tistory.com/23</guid>
      <comments>https://loan-project.tistory.com/23#entry23comment</comments>
      <pubDate>Fri, 8 May 2026 10:50:07 +0900</pubDate>
    </item>
    <item>
      <title>기본 데이터 처리 - ORDER BY</title>
      <link>https://loan-project.tistory.com/22</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ORDER BY, 다 같은 정렬이 아닙니다.&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SQL 을 작성할 때 , ORDER BY 는 아무 생각 없이 붙이기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 MySQL 내부에서는 같은 ORDER BY 라도 처리하는 방식이 완전히 달라집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MySQL 의 일반적인 SQL 실행 흐름은 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1293&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRUZWF/dJMcahxyN6R/BM6KQmjfaobS5RzIvrbZok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRUZWF/dJMcahxyN6R/BM6KQmjfaobS5RzIvrbZok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRUZWF/dJMcahxyN6R/BM6KQmjfaobS5RzIvrbZok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRUZWF%2FdJMcahxyN6R%2FBM6KQmjfaobS5RzIvrbZok%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;1293&quot; height=&quot;210&quot; data-origin-width=&quot;1293&quot; data-origin-height=&quot;210&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;size18&quot;&gt;ORDER BY 의 처리 방식은 옵티마이저가 실행계획을 수립하는 시점에 결정 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 옵티마이저는 상황에 따라 전혀 다른 방식으로 정렬을 처리 합니다. 똑같이 ORDER BY 를 작성했더라도, 인덱스를 활용해 정렬을 생략하기도 하고, 별도의 정렬 작업을 수행하기도 합니다. 이 차이가 쿼리 성능에 큰 영향을 미칩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 글에서는 MySQL 이 ORDER BY 를 처리하는 방식에 대해 다뤄보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;주요 내용은 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정렬에 사용하는 소트버퍼와 정렬 알고리즘&amp;nbsp;&lt;/li&gt;
&lt;li&gt;ORDER BY 에 대해 정렬이 이루어지는 3가지 방식&lt;/li&gt;
&lt;li&gt;실제로 어떤 정렬이 선택됐는지 실행 계획으로 확인하는 방법&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;소트 버퍼&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 소트 버퍼란 ?&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MySQL 이 정렬을 수행할 때는 별도의 메모리 공간을 사용합니다. 이 공간을 소트 버퍼(Sort Buffer) 라고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;소트 버퍼는 정렬이 필요한 시점에만 할당되며, 쿼리가 완료되면 즉시 반환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정렬해야 할 크기에 따라 가변적으로 증가하며, 사용할 수 있는 최대 크기는 sort_buffer_size 시스템 변수로 설정 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778045555775&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 현재 sort_buffer_size 확인
SHOW VARIABLES LIKE 'sort_buffer_size';&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;129&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kWUc8/dJMcacXiiqa/y0zWSE1CZOrY5gj0j4Txk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kWUc8/dJMcacXiiqa/y0zWSE1CZOrY5gj0j4Txk1/img.png&quot; data-alt=&quot;현재 소트버퍼 크기는 262144 bytes = 256KB&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kWUc8/dJMcacXiiqa/y0zWSE1CZOrY5gj0j4Txk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkWUc8%2FdJMcacXiiqa%2Fy0zWSE1CZOrY5gj0j4Txk1%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;336&quot; height=&quot;129&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;129&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재 소트버퍼 크기는 262144 bytes = 256KB&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 멀티 머지란 ?&amp;nbsp;&lt;/b&gt;&lt;/h3&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;1732&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byA2vQ/dJMcabxjcyO/d7dNuEc7xlvdlbphunmL50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byA2vQ/dJMcabxjcyO/d7dNuEc7xlvdlbphunmL50/img.png&quot; data-alt=&quot;멀티 머지 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byA2vQ/dJMcabxjcyO/d7dNuEc7xlvdlbphunmL50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyA2vQ%2FdJMcabxjcyO%2Fd7dNuEc7xlvdlbphunmL50%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;1732&quot; height=&quot;788&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;멀티 머지 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그렇다면 정렬해야 할 레코드의 크기가 소트 버퍼보다 크다면 어떻게 될까요 ?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 그림 처럼 소트 버퍼의 크기만큼 레코드를 나눠서 정렬 한후 , 디스크에 기록하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;디스크에는 단위 별로 정렬된 결과가 존재하게 되는데 이를 런이라 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉 , 런 내부에서는 레코드가 정렬된 상태이고 , 레코드의 디스크 전체에서는 정렬이 되지 않은 상태 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;마지막으로 런들을 정렬하여 최종 정렬된 레코드를 반환하는데 이러한 런들의 정렬을 멀티 머지라고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 멀티 머지 확인 해보기&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;멀티 머지는 Sort_merge_passes 상태 변수로 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778048574019&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- Sort_merge_passes 확인
SHOW STATUS LIKE 'Sort_merge_passes';&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;size18&quot;&gt;그렇다면 이제 실제로 멀티 머지를 발생시키고 , 이를 확인해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;lt;테이블 생성&amp;gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1778048292553&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE loan (
    loan_id      INT AUTO_INCREMENT PRIMARY KEY,
    customer_id  INT NOT NULL,
    loan_type    VARCHAR(20) NOT NULL,   -- 'MORTGAGE','CREDIT','AUTO','STUDENT'
    loan_amount  DECIMAL(15,2) NOT NULL,
    interest_rate DECIMAL(5,2) NOT NULL,
    start_date   DATE NOT NULL,
    status       VARCHAR(10) NOT NULL,   -- 'ACTIVE','CLOSED','OVERDUE'
    memo         VARCHAR(200)            -- 레코드 크기 늘리기 위한 컬럼
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;lt;데이터 삽입 (5만 건)&amp;gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1778048351538&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INSERT INTO loan (customer_id, loan_type, loan_amount, interest_rate, start_date, status, memo)
WITH RECURSIVE cte AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM cte WHERE n &amp;lt; 50000
)
SELECT
    FLOOR(RAND() * 100000) + 1,
    ELT(FLOOR(RAND() * 4) + 1, 'MORTGAGE','CREDIT','AUTO','STUDENT'),
    ROUND(RAND() * 490000000 + 10000000, 2),
    ROUND(RAND() * 15 + 1, 2),
    DATE_ADD('2015-01-01', INTERVAL FLOOR(RAND() * 3000) DAY),
    ELT(FLOOR(RAND() * 3) + 1, 'ACTIVE','CLOSED','OVERDUE'),
    REPEAT('X', 150)   -- memo를 150자로 채워 레코드를 크게 만듦
FROM cte;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;lt;쿼리 실행&amp;gt;&amp;nbsp;&lt;/h4&gt;
&lt;pre id=&quot;code_1778048963551&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 현재 Sort_merge_passes 초기화 확인
FLUSH STATUS;

-- sort_buffer_size 세션에서만 줄이기 (더 확실한 실험을 위해)
SET SESSION sort_buffer_size = 32768;  -- 32KB

-- 멀티 머지 유발 쿼리 (인덱스 없는 컬럼으로 정렬)
SELECT loan_id, customer_id, loan_amount, interest_rate, status
FROM loan
ORDER BY loan_amount DESC;

-- Sort_merge_passes 확인
SHOW STATUS LIKE 'Sort_merge_passes';&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;lt;실행 결과&amp;gt;&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;323&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nTjTk/dJMcacbV9v4/316yp5FgnR2dzZ15RvRXD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nTjTk/dJMcacbV9v4/316yp5FgnR2dzZ15RvRXD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nTjTk/dJMcacbV9v4/316yp5FgnR2dzZ15RvRXD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnTjTk%2FdJMcacbV9v4%2F316yp5FgnR2dzZ15RvRXD0%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;323&quot; height=&quot;140&quot; data-origin-width=&quot;323&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실행 결과 디스크에 10개의 소트버퍼 정렬결과가 저장되고 , 이를 멀티 머지 했음을 알 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정렬 알고리즘&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정렬은 소트 버퍼에 무엇을 담느냐에 따라 싱글 패스와 투 패스 방식으로 나뉩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예시 쿼리가 주어질 때 , 싱글 패스와 투 패스 방식에서 어떤 데이터가 소트 버퍼에 담기는지 비교하면서 알아보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1778050241118&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT customer_id, loan_type, loan_amount, status, memo
FROM loan
ORDER BY loan_amount DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;싱글 패스 정렬 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT 대상이 되는 &lt;b&gt;모든 컬럼을 소트 버퍼에 담아&lt;/b&gt; 정렬을 수행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PWoNG/dJMb99M07a3/JkKgyIB3CughJfYVDqD7Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PWoNG/dJMb99M07a3/JkKgyIB3CughJfYVDqD7Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PWoNG/dJMb99M07a3/JkKgyIB3CughJfYVDqD7Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPWoNG%2FdJMb99M07a3%2FJkKgyIB3CughJfYVDqD7Dk%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;625&quot; height=&quot;428&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;648&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;위 그림에서 알 수 있듯이 , 싱글 패스 방식에서는 select 하는 열의 모든 데이터를 소트 버퍼에 담고 정렬합니다.&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;테이블 읽기 -&amp;gt; 소트 버퍼에 (정렬 키 + SELECT 컬럼 전부) 적재 -&amp;gt; 정렬 -&amp;gt; 결과 반환&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;투 패스 정렬 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGryrx/dJMcadPoeeB/aAYfKjKh55DHYeozVAuda1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGryrx/dJMcadPoeeB/aAYfKjKh55DHYeozVAuda1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGryrx/dJMcadPoeeB/aAYfKjKh55DHYeozVAuda1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGryrx%2FdJMcadPoeeB%2FaAYfKjKh55DHYeozVAuda1%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;1768&quot; height=&quot;460&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;투 패스 정렬 방식은 2단계로 이루어집니다.&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;테이블 읽기 -&amp;gt; 소트 버퍼에 (정렬 키 + PK) 만 적재 -&amp;gt; 정렬&amp;nbsp;&lt;/li&gt;
&lt;li&gt;정렬된 PK 순서대로 테이블 다시 읽기 -&amp;gt; SELECT 컬럼 가져 오기 -&amp;gt; 결과 반환&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기본 방향 - 싱글 패스가 기본&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;투 패스 방식은 MySQL의 예전 정렬 방식입니다. 테이블을 두 번 읽어야 한다는 점에서 본질적으로 비효율적이기 때문에, 최신 버전의 MySQL은 &lt;b&gt;기본적으로 싱글 패스 방식을 사용&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 싱글 패스는 SELECT 대상 컬럼 전부를 소트 버퍼에 담기 때문에, 레코드가 클수록 소트 버퍼를 빠르게 소진합니다. 그래서 옵티마이저는 특정 조건에서는 오히려 투 패스 방식을 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;옵티마이저가 투 패스를 선택하는 2 가지 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;레코드 크기가 max_length_for_sort_data 보다 클 때&amp;nbsp;&lt;/li&gt;
&lt;li&gt;BOLB 이나 TEXT 타입 컬럼이 SELECT 대상에 포함될 때&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다만 MySQL 8.0.20 이후 부터는 두가지 경우 모두 투 패스 방식을 사용 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MySQL 8.0.20 공식 릴리즈 노트에서는 아래와 같이 명시하고 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-20.html&quot;&gt;Changes in MySQL 8.0.20&lt;/a&gt; &quot;The max_length_for_sort_data system variable is now deprecated due to optimizer changes that make it obsolete and of no effect.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;옵티마이저 내부 변경으로 인해 max_length_for_sort_data 를 기준으로 투 패스를 선택하는 로직 자체가 제거됐습니다. 따라서 이 변수를 아무리 작게 설정해도 투 패스로 전환되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;같은 릴리즈 노트에서 아래와 같이 명시합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Previously, including any column of a blob type larger than TINYBLOB or BLOB as the payload in an ordering operation caused the server to revert to sorting row IDs only. The server now converts columns of these types into packed addons in such cases, just as it does TINYBLOB and BLOB columns, which in testing showed a significant performance increase.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;8.0.20 이후 BLOB, TEXT, JSON, GEOMETRY 컬럼이 포함되더라도 싱글 패스로 처리합니다. 단, &lt;b&gt;MEDIUMBLOB 과 LONGBLOB 은 여전히 예외&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;직접 확인해보기 - 어떤 방식이 선택됐는지 보는 법 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;optimizer_trace 를 활용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;lt;1&amp;gt; optimizer_trace 활성화 및 쿼리 실행&lt;/p&gt;
&lt;pre id=&quot;code_1778055799809&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- optimizer_trace 활성화
SET SESSION optimizer_trace = 'enabled=on';

-- 확인할 쿼리 실행
SELECT customer_id, loan_type, loan_amount, status, memo
FROM loan
ORDER BY loan_amount DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;lt;2&amp;gt; sort_mode 확인&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778055879628&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
  JSON_EXTRACT(trace, '$.steps[*].join_execution.steps[*].filesort_summary')
FROM information_schema.OPTIMIZER_TRACE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;sort_mode 에 따른 , 정렬 방식은 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kTc06/dJMcadBPFvW/KM67NjZss5J1ci1VGURv00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kTc06/dJMcadBPFvW/KM67NjZss5J1ci1VGURv00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kTc06/dJMcadBPFvW/KM67NjZss5J1ci1VGURv00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkTc06%2FdJMcadBPFvW%2FKM67NjZss5J1ci1VGURv00%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;745&quot; height=&quot;160&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778056038878&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;| [{
&quot;key_size&quot;: 8, &quot;row_size&quot;: 944, 
&quot;sort_mode&quot;: &quot;&amp;lt;fixed_sort_key, additional_fields&amp;gt;&quot;, -- 싱글 패스 확인
&quot;num_rows_found&quot;: 50000, 
&quot;sort_algorithm&quot;: &quot;std::sort&quot;, 
&quot;memory_available&quot;: 32768, 
&quot;peak_memory_used&quot;: 10472, 
&quot;num_rows_estimate&quot;: 49392, 
&quot;max_rows_per_buffer&quot;: 11, 
&quot;unpacked_addon_fields&quot;: 
&quot;using_priority_queue&quot;, 
&quot;num_initial_chunks_spilled_to_disk&quot;: 0}] |&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;옵티마이저의 ORDER BY 선택&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ORDER BY 가 포함된 쿼리는 상황에 따라서 아래 3가지 방식 중 하나로 처리 됩니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;옵티마이저는 가장 비용이 낮은 방식을 선택하며 , 오른쪽으로 갈수록 비용이 커집니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbn84S/dJMcadID7qC/FvxDj9z87rF2SLb7p7Lie0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbn84S/dJMcadID7qC/FvxDj9z87rF2SLb7p7Lie0/img.png&quot; data-alt=&quot;옵티마이저의 3가지 선택&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbn84S/dJMcadID7qC/FvxDj9z87rF2SLb7p7Lie0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcbn84S%2FdJMcadID7qC%2FFvxDj9z87rF2SLb7p7Lie0%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;1534&quot; height=&quot;544&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;옵티마이저의 3가지 선택&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 방법으로 ORDER BY 를 처리하였을 때 , 옵티마이저의 실행계획 Extra 컬럼의 내용은 아래와 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;b&gt;정렬 처리 방법&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;b&gt;실행 계획 Extra 컬럼 내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;(1) 인덱스를 이용한 정렬&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;(2) 드라이빙 테이블만 정렬&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Using filesort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;(3) 임시 테이블을 이용한 정렬&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Using temporary; Using filesort&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;각각의 방법을 설명할 때 , 아래의 테이블과 인덱스를 예시로 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778115206684&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 고객 테이블
CREATE TABLE customer (
    customer_id  INT AUTO_INCREMENT PRIMARY KEY,
    name         VARCHAR(50) NOT NULL,
    region       VARCHAR(20) NOT NULL,
    INDEX idx_region (region)
);

-- 대출 테이블
CREATE TABLE loan (
    loan_id      INT AUTO_INCREMENT PRIMARY KEY,
    customer_id  INT NOT NULL,
    loan_amount  DECIMAL(15,2) NOT NULL,
    created_at   DATETIME NOT NULL,
    INDEX idx_customer_amount (customer_id, loan_amount),
    INDEX idx_created_at (created_at)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인덱스를 이용한 정렬&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;1&amp;gt; 조건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 이용한 정렬이 가능하려면 아래 두가지 조건을 만족해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORDER BY 에 명시된 컬럼이 드라이빙 테이블에 속해야 합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;ORDER BY 의 순서대로 생성된 인덱스가 존재해야 합니다.&amp;nbsp;&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;별도의 정렬 작업이 발생하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;2&amp;gt; 예시 쿼리&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778116191297&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT l.loan_id, l.loan_amount, c.name
FROM loan l
JOIN customer c ON l.customer_id = c.customer_id
ORDER BY l.customer_id;&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;loan 테이블에 idx_customer_amount (customer_id , loan_amount) 인덱스가 존재하고, ORDER BY 컬럼인 customer_id 가 드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이빙 테이블인 loan 소속이므로 인덱스를 이용한 정렬이 가능합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mf5yS/dJMcagrR9iZ/xyo3jevF4EGBJw8buxade0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mf5yS/dJMcagrR9iZ/xyo3jevF4EGBJw8buxade0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mf5yS/dJMcagrR9iZ/xyo3jevF4EGBJw8buxade0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMf5yS%2FdJMcagrR9iZ%2Fxyo3jevF4EGBJw8buxade0%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;644&quot; height=&quot;441&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;3&amp;gt; 실행계획&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccksAV/dJMcadBP0GZ/zrUb4IXMq5KbLiKnYZZ48K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccksAV/dJMcadBP0GZ/zrUb4IXMq5KbLiKnYZZ48K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccksAV/dJMcadBP0GZ/zrUb4IXMq5KbLiKnYZZ48K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccksAV%2FdJMcadBP0GZ%2FzrUb4IXMq5KbLiKnYZZ48K%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;1170&quot; height=&quot;128&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행계획에서 확인할 수 있는 핵심은 두 가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;b&gt;loan 이 드라이빙 테이블로 선택&lt;/b&gt;됐습니다. ORDER BY l.customer_id 가 드라이빙 테이블 소속이고, idx_customer_amount 인덱스가 customer_id 순서로 정렬되어 있기 때문에 인덱스를 순서대로 읽는 것만으로 정렬이 완성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째 , Using filesort 가 없습니다. 별도의 정렬 작업이 전혀 발생하지 않았다는 의미입니다. 드라이빙 테이블인 loan 의 Extra 에는 Using index 만 표시되고, 드리븐 테이블인 customer 는 customer_id 조회 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;드라이빙 테이블만 정렬&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;1&amp;gt; 조건&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORDER BY 에 명시된 컬럼이 드라이빙 테이블에 속해야 합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;단 , 인덱스를 이용한 정렬은 불가능한 상황 입니다. (정렬 컬럼에 인덱스가 없거나, 인덱스 컬럼 순서가 다름)&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 옵티마이저는 드라이빙 테이블을 먼저 Filesort 로 정렬한 뒤, 정렬된 순서대로 드리븐 테이블과 조인합니다.&lt;/p&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;lt;2&amp;gt; 예시 쿼리&lt;/p&gt;
&lt;pre id=&quot;code_1778119968003&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT l.loan_id, l.loan_amount, c.name
FROM loan l
JOIN customer c ON l.customer_id = c.customer_id
ORDER BY l.loan_amount DESC;&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;드라이빙 테이블로 loan 이 선택될 경우 , loan_amount 는 단독으로 정렬에 활용할 수 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(customer_id , loan_amount) 인덱스는 첫번째 컬럼인 customer_id 없이는 인덱스 정렬이 불가능 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;1280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zJzqU/dJMcad2Wi42/ncjjaB7KI5VAvY1Ew6HU51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zJzqU/dJMcad2Wi42/ncjjaB7KI5VAvY1Ew6HU51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zJzqU/dJMcad2Wi42/ncjjaB7KI5VAvY1Ew6HU51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzJzqU%2FdJMcad2Wi42%2FncjjaB7KI5VAvY1Ew6HU51%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;1856&quot; height=&quot;1280&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;1280&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;ORDER BY loan_amount 는 드라이빙 테이블인 loan 소속 컬럼이지만, loan_amount 단독으로는 인덱스를 정렬에 활용할 수 없습니다. 이 경우 옵티마이저는 위 그림과 같이 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 드라이빙 테이블인 loan 의 레코드를 소트 버퍼에 올려 loan_amount 기준으로 정렬합니다. 정렬이 완료되면 그 순서대로 드리븐 테이블인 customer 와 1건씩 조인하여 결과를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;조인 이전에 정렬이 끝난다&lt;/b&gt;는 점입니다. 드리븐 테이블인 customer 는 정렬에 전혀 관여하지 않고, 정렬된 순서대로 단순히 조회될 뿐입니다.&lt;/p&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;lt;3&amp;gt; 실행 계획&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1275&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKSftJ/dJMb99TONJd/WDpx76xS1CXNOMkix7U6TK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKSftJ/dJMb99TONJd/WDpx76xS1CXNOMkix7U6TK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKSftJ/dJMb99TONJd/WDpx76xS1CXNOMkix7U6TK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKSftJ%2FdJMb99TONJd%2FWDpx76xS1CXNOMkix7U6TK%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;1275&quot; height=&quot;126&quot; data-origin-width=&quot;1275&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;Using filesort&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loan_amount 기준 정렬을 인덱스로 처리할 수 없기 때문에, &lt;b&gt;조인 이전에 드라이빙 테이블인 loan 을 소트 버퍼에서 먼저 정렬&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬이 완료된 뒤 그 순서대로 customer 와 조인하기 때문에, 드리븐 테이블인 customer 의 Extra 는 NULL 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Using index 는 idx_customer_amount 인덱스 레인지 스캔만으로 필요한 데이터를 모두 가져올 수 있어서 표시된 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;임시 테이블을 이용한 정렬&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;1&amp;gt; 조건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 이용한 정렬도, 드라이빙 테이블만 정렬하는 것도 불가능한 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 ORDER BY 에 명시된 컬럼이 드리븐 테이블 소속일 때 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인을 먼저 수행하기 전까지는 정렬 기준 컬럼의 값을 알 수 없기 때문에, 옵티마이저는 &lt;b&gt;조인 결과 전체를 임시 테이블에 저장한 뒤 정렬&lt;/b&gt;합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬해야 할 레코드 건수가 가장 많기 때문에 가장 느린 정렬 방법 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;2&amp;gt; 예시 쿼리&lt;/p&gt;
&lt;pre id=&quot;code_1778127871669&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT l.loan_id, l.loan_amount, c.name
FROM loan l
JOIN customer c ON l.customer_id = c.customer_id
ORDER BY c.name DESC;&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;2108&quot; data-origin-height=&quot;1072&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRZDCw/dJMcabxjTC0/uZq5A43dDUiRVunuzpxybk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRZDCw/dJMcabxjTC0/uZq5A43dDUiRVunuzpxybk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRZDCw/dJMcabxjTC0/uZq5A43dDUiRVunuzpxybk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRZDCw%2FdJMcabxjTC0%2FuZq5A43dDUiRVunuzpxybk%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;2108&quot; height=&quot;1072&quot; data-origin-width=&quot;2108&quot; data-origin-height=&quot;1072&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;ORDER BY c.name 은 드리븐 테이블인 customer 소속 컬럼입니다. 조인을 수행하기 전까지는 name 값을 알 수 없기 때문에, 드라이빙 테이블을 먼저 정렬하는 것이 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 옵티마이저는 위 그림과 같이 동작합니다. 먼저 loan 과 customer 를 조인한 뒤, 그 결과 전체를 임시 테이블에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 임시 테이블을 소트 버퍼에 올려 name 기준으로 정렬하고 최종 결과를 반환합니다.&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;1397&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cX7LNm/dJMcacpuwYY/OAPY9uRe97eNatBS3xpv41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cX7LNm/dJMcacpuwYY/OAPY9uRe97eNatBS3xpv41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cX7LNm/dJMcacpuwYY/OAPY9uRe97eNatBS3xpv41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcX7LNm%2FdJMcacpuwYY%2FOAPY9uRe97eNatBS3xpv41%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;1397&quot; height=&quot;128&quot; data-origin-width=&quot;1397&quot; data-origin-height=&quot;128&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;실행 계획에서 Using temporary; Using filesort 가 표시되어 임시 테이블을 쓰고 , 소트 버퍼로 정렬을 한다는 것을 알 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 MySQL이 ORDER BY 를 처리하는 방식을 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬은 단순히 ORDER BY 를 붙이는 것으로 끝나지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 상황에 따라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인덱스를 이용한 정렬&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;드라이빙 테이블만 정렬 &lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;임시 테이블을 이용한 정렬 &lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중 하나를 선택하며, 이 선택이 쿼리 성능에 직접적인 영향을 미칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가장 이상적인 상황은 인덱스를 이용한 정렬입니다.&lt;/b&gt; 별도의 정렬 작업 없이 인덱스 순서대로 읽는 것만으로 정렬이 완성되기 때문입니다. 이를 위해서는 ORDER BY 컬럼이 드라이빙 테이블 소속이어야 하고, 해당 순서로 생성된 인덱스가 존재해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이빙 테이블만 정렬하는 경우라면, 조인 이전에 정렬이 끝나기 때문에 처리 비용이 크지 않을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Using temporary; Using filesort 가 함께 나타난다면 주의가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인 결과 전체를 임시 테이블에 저장한 뒤 정렬하기 때문에, 데이터가 많아질수록 성능 저하가 커집니다.&lt;/p&gt;</description>
      <category>기술 파보기/MySQL</category>
      <author>watanabe3</author>
      <guid isPermaLink="true">https://loan-project.tistory.com/22</guid>
      <comments>https://loan-project.tistory.com/22#entry22comment</comments>
      <pubDate>Wed, 6 May 2026 11:49:24 +0900</pubDate>
    </item>
  </channel>
</rss>