<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>토마토의 개발일지</title>
    <link>https://mntdev.tistory.com/</link>
    <description>기록하며 공부하는 블로그</description>
    <language>ko</language>
    <pubDate>Fri, 8 May 2026 04:54:27 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>tomato_dev</managingEditor>
    <image>
      <title>토마토의 개발일지</title>
      <url>https://tistory1.daumcdn.net/tistory/5148197/attach/225192d1eb4544f2bf46884f7e5ee492</url>
      <link>https://mntdev.tistory.com</link>
    </image>
    <item>
      <title>[Qdrant Vector DB] 벡터 DB에서 페이징 처리 설계</title>
      <link>https://mntdev.tistory.com/152</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w3Sf5/dJMcacItwV3/pKJGMpIG1fRJJskkwY9clk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w3Sf5/dJMcacItwV3/pKJGMpIG1fRJJskkwY9clk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w3Sf5/dJMcacItwV3/pKJGMpIG1fRJJskkwY9clk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw3Sf5%2FdJMcacItwV3%2FpKJGMpIG1fRJJskkwY9clk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;752&quot; height=&quot;200&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;1. 문제 인식 &amp;ndash; &amp;ldquo;검색 결과가 이상해요&amp;rdquo;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Qdrant Vector DB에서 페이징을 설계하는 중 FE에서 검색 시 응답받는 totalElements가 이상하다. 라는 피드백을 받았습니다. 매번 totalElements가 다르게 응답되고 있었고, 그 문제를 해결하는 과정을 담아보았습니다.&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;offset 기반 페이징으로 개발
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &lt;code&gt;page=3&amp;amp;pageSize=20&lt;/code&gt; &amp;rarr; 41~60번째 결과&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;현재 검색 조건(검색어 + 필터)에 대해
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;총 몇 건인지(totalElements)&lt;/li&gt;
&lt;li&gt;총 몇 페이지인지(totalPages)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는, 우리가 사용하는 Qdrant가&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;벡터 검색(QueryPoints)에 대해 offset/limit 페이징은 지원하지만&lt;/li&gt;
&lt;li&gt;해당 벡터 + 필터 조합으로 &amp;ldquo;총 몇 건인지&amp;rdquo;를 알려주는 API는 제공하지 않는다는 점입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 실제로 어떤 제약이 있는지, 왜 이런 제약이 있는지, 그리고 설계 측면에서 어떻게 풀었는지에 대해 정리하려고 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. Qdrant에서 제공하는 것들 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 현재 Qdrant가 제공하는 페이징 관련 기능을 정리하면 아래와 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.1. 벡터 검색(QueryPoints) &amp;ndash; offset/limit 지원&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 주로 사용하는 &lt;code&gt;query_points&lt;/code&gt; API는 다음을 지원합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;벡터 쿼리: &lt;code&gt;query = nearest(vec)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;옵션 필터: payload 기반 필터 (&lt;code&gt;filter&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;페이징 파라미터: &lt;code&gt;limit&lt;/code&gt;, &lt;code&gt;offset&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;벡터 + 필터 + offset/limit&amp;rdquo; 조합으로 페이지 데이터를 가져오는 것은 지원합니다.&lt;br /&gt;우리가 실제 코드에서 쓰는 부분도 이것입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Points.QueryPoints.Builder builder = Points.QueryPoints.newBuilder()
    .setCollectionName(collectionName)
    .setQuery(QueryFactory.nearest(vec)) // 벡터 검색
    .setLimit(limit)
    .setOffset(offset)
    .setWithPayload(...)
    .setParams(...)
    .setFilter(filter);                 // (필터가 있다면)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쪽은 동작에 문제가 없습니다.&lt;br /&gt;지정한 offset/limit에 맞게 해당 페이지에 해당하는 결과는 잘 내려옵니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.2. Count API &amp;ndash; 필터 기반 개수 세기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도로 Qdrant에는 &lt;code&gt;count&lt;/code&gt; API가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;filter&lt;/code&gt; + &lt;code&gt;exact&lt;/code&gt; 파라미터만 받습니다.&lt;/li&gt;
&lt;li&gt;벡터(query)는 아예 받을 수 있는 필드가 없습니다.&lt;/li&gt;
&lt;li&gt;결과는 이 컬렉션에서 해당 필터에 맞는 포인트가 몇 개인지를 반환하는게 전부 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public long countPoints(Points.Filter filter) {
    ensureInitialized();
    try {
        return qdrantClient.countAsync(collectionName, filter, true, Duration.ofSeconds(5))
                .get(5, TimeUnit.SECONDS);
    } ...
}&lt;/code&gt;&lt;/pre&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;code&gt;countPoints(filter)&lt;/code&gt; &amp;rarr; 필터 기준 전체 개수&lt;/li&gt;
&lt;li&gt;&lt;code&gt;searchText(vec, limit, offset, filter)&lt;/code&gt; &amp;rarr; 벡터 + 필터 기준 특정 페이지 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Qdrant는 설계상 필터 기반 전체 개수 세기와 벡터 기반 top-K 검색 2개의 API를 서로 다른 API로 분리해 두었습니다. 카운트를 하는데 검색어(query/벡터)는 받을 수 없는거죠.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. 요구사항과의 충돌 지점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 것은 다음과 같은 형태였습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;offset 기반 전통적인 페이징 UI
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &lt;code&gt;Prev / 1 2 3 &amp;hellip; 22 / Next&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;페이지 기준이 되는 검색 조건
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;벡터(검색어 임베딩) + 여러 필터 조건&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그리고&amp;hellip;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 조건에 대한 정확한 totalElements (총 몇 건인지)&lt;/li&gt;
&lt;li&gt;totalPages 계산 (&lt;code&gt;ceil(totalElements / pageSize)&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 앞에서 본 것처럼 Qdrant는 &lt;code&gt;query_points&lt;/code&gt; 응답에 total count를 넣어주지 않고, &lt;code&gt;count&lt;/code&gt; API는 벡터를 모릅니다(필터만 적용).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 이번 이슈의 핵심입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. 그렇다면 totalElements를 못 구하는 이유는?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 자연스럽게 나오는 질문이 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;그럼 벡터 검색할 때, 그냥 total도 같이 계산해서 내려주면 안 되나?&amp;rdquo;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 Qdrant가 사용하는 벡터 검색 방식(HNSW 기반 ANN)의 특성과 관련이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;1696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VRBpN/dJMcagRC73h/diiO4kaPAtnkTPKyQFs0Ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VRBpN/dJMcagRC73h/diiO4kaPAtnkTPKyQFs0Ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VRBpN/dJMcagRC73h/diiO4kaPAtnkTPKyQFs0Ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVRBpN%2FdJMcagRC73h%2FdiiO4kaPAtnkTPKyQFs0Ak%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;2560&quot; height=&quot;1696&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;1696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HNSW/ANN은 top-K 가까운 이웃을 빠르게 찾기 위한 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리로 매칭되는 전체 개수를 정확히 세라&amp;rdquo;는 건 사실상 모든 후보를 끝까지 탐색하거나, 그에 준하는 연산이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 필터까지 섞이고, 분산/샤딩 구조까지 고려하면? 매 쿼리마다 &amp;ldquo;정확한 total&amp;rdquo;을 내려주는 것은 성능&amp;middot;리소스 측면에서 부담이 상당하게 되는거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Qdrant 쪽 설계는 &amp;ldquo;정확한 total이 필요한 카운트는 payload 인덱스를 활용한 filter 기준 count로 분리&amp;rdquo;하고, 벡터 쿼리는 빠른 top-K 검색에 집중하기 때문에 둘을 섞어서 &amp;ldquo;벡터 + 필터에 대한 정확 total&amp;rdquo;까지 한 번에 제공하는 기능은 아직 제공하지 않는 방향으로 굳어진 상태입니다. 일반적으로 생각하는 검색처럼 &lt;b&gt;&quot;토마토&quot;를 검색했을 때 검색 결과가 딱 312개 나오는 것 과는 다른거죠.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. 어떻게 기준을 정할 것인가?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 현실적인 선택을 해야 합니다.&lt;br /&gt;현재 Qdrant의 제약 안에서 할 수 있는 선택지는 크게 세 가지였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5.1. 선택지 1 &amp;ndash; 필터 기준 total을 &amp;ldquo;근사값&amp;rdquo;으로 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 UI 상으로는 &amp;ldquo;이 검색 조건에 대해 총 N건&amp;rdquo;이라고 보이지만, 실제 의미는 &amp;ldquo;필터까지 만족하는 데이터가 N건이고, 그 중에서 벡터 기준으로 상위 결과를 페이지 단위로 보여주는 중&amp;rdquo;에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점은 기존 offset 페이징 UX (페이지 번호, totalPages 등)를 유지할 수 있고, Qdrant가 제공하는 API 범위 안에서 구현 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc11Yk/dJMcah34z6P/t48VqD8vKHfauNX89fKIm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc11Yk/dJMcah34z6P/t48VqD8vKHfauNX89fKIm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc11Yk/dJMcah34z6P/t48VqD8vKHfauNX89fKIm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc11Yk%2FdJMcah34z6P%2Ft48VqD8vKHfauNX89fKIm1%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;720&quot; height=&quot;107&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;107&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점은 &amp;ldquo;벡터까지 반영된 완전히 정확한 total&amp;rdquo;은 아니라는 점입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5.2. 선택지 2 &amp;ndash; totalElements를 과감히 포기한 UX&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;query_points(limit, offset)&lt;/code&gt; 만 사용하고 UI에서는 &amp;ldquo;총 N건 / 총 M페이지&amp;rdquo;를 아예 표시하지 않고, &amp;ldquo;다음 페이지 / 더 보기&amp;rdquo;만 제공하는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 페이지 여부 판단은 &lt;code&gt;limit = pageSize + 1&lt;/code&gt;로 요청 후&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결과가 &lt;code&gt;pageSize + 1&lt;/code&gt;개 &amp;rarr; 다음 페이지 있음&lt;/li&gt;
&lt;li&gt;결과가 &lt;code&gt;pageSize&lt;/code&gt; 이하 &amp;rarr; 마지막 페이지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 백엔드/프론트 모두 구현이 단순하고, Qdrant 설계와도 합이 괜찮습니다. 하지만 다만 &amp;ldquo;총 몇 건이냐&amp;rdquo;를 사용자에게 보여주지 못한다는 UX상의 trade-off가 존재합니다. 하지만 필터되는 데이터는 카운트가 가능하기 때문에 무조건 total이 맞아야해! 라는 요구사항이 아니면 좋은 선택지라고 생각했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5.3. 선택지 3 &amp;ndash; 전체를 다 가져와서 세기 (특수 케이스)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적으로는 &lt;code&gt;limit&lt;/code&gt;을 매우 크게 잡고(예: 수만 건), 모든 페이지를 끝까지 돌면서 결과를 수집/카운트한 뒤, 그걸 기반으로 custom 페이징을 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데이터가 커지면 네트워크/메모리/CPU 모두 말도 안되게 필요할거고, 일정 규모 이상 되는 운영 환경에서는 사실상 사용할 수 없는 방식입니다.(검색 한번에 10분이 넘는다면..?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 이유로 벡터 + 필터 쿼리에 대한 정확한 total count를 매 요청마다 계산하는 건, ANN/HNSW 기반 벡터 DB 설계에서 속도&amp;middot;리소스&amp;middot;알고리즘 특성상 비용이 너무 크고, 개념적으로도 애매해서 아직 기능을 넣지 않은 것으로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 선택지는 과감하게 제외하는게 현실적으로 정신적으로 이롭습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. 정렬도 문제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vector DB로 검색을 구현하는 과정에서 한 가지 더 짚고 넘어가야 할 점이 &lt;b&gt;&amp;ldquo;정렬의 의미&amp;rdquo;&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급했듯, Vector DB의 기본 역할은 임베딩된 쿼리 벡터와 가장 근접한 topK 데이터를 찾는 것입니다.&lt;br /&gt;즉, 검색어(벡터)와의 관련도(유사도) 순으로 이미 정렬된 결과 집합을 반환합니다.&lt;br /&gt;이 단계에서의 정렬 기준은 오로지 벡터 유사도(score)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 여기서 한 발 더 나아가, 많은 서비스가 원하는 것처럼 벡터로 비슷한 것만 골라오고 동시에 생성일자, 이름 같은 필드 기준으로 정확하게 정렬된 데이터를 제공하고 싶어할 때입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 제가 처음에 했던 생각이&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;상위 N개만 벡터로 검색한 뒤, 그 안에서 가격순&amp;middot;이름순으로 다시 정렬하면 되는 것 아닌가?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인데, 이 방식은 기술적으로는 가능하지만, 의미상 전체 데이터에 대한 가격순/이름순 정렬이라고 부를 수는 없습니다. 벡터 검색으로 가져온 것은 전체 데이터 중 상위 N개의 후보 집합일 뿐이고 그 안에서만 가격순으로 재배치하는 것은 Top-N 후보 재랭킹이지, 전체 결과의 전역 정렬(global sort)이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해 사용자 입장에서는 화면에 &lt;code&gt;가격순&lt;/code&gt;이라고 써 있으면 당연히 전체 결과가 가격 오름차순으로 정렬되어 있고 1페이지, 2페이지, 3페이지를 아무리 넘어가도 뒤 페이지에서 더 싼 상품이 갑자기 튀어나오지 않는다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Vector DB에서 &lt;b&gt;topK + 재랭킹&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;1페이지에는 특정 범위의 후보 N개가 들어 있고&lt;/li&gt;
&lt;li&gt;2페이지를 넘어갔을 때, 1페이지보다 더 낮은 가격/더 이른 날짜가 뒤에서 튀어나올 수 있는 구조가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 개발하면 사실상 기대와 완전히 어긋나는 동작이고, 사용자 입장에서는 엥? 할 수 밖에 없게 동작하게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 데이터에 대해 벡터 유사도 + 가격/날짜까지 모두 반영한 전역 정렬을 하려면 매우 큰 후보 집합에 대해 벡터 distance/score를 계산하고 payload(가격, 날짜 등)를 함께 읽은 뒤 다시 전체 정렬을 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 ANN이라는 근사 최근접 탐색을 사용하는 Vector DB의 설계 목적(빠르게 topK 찾기)과 정면으로 충돌합니다. 전용 Vector DB 하나에 유사도 + 완전한 가격/날짜 정렬까지 모두 맡기면, 사실상 &lt;b&gt;전역 스캔에 가까운 비용을 감수해야 하는거라서 Vector DB의 가장 큰 장점인 유사도 검색의 의미가 퇴색&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 몇 점 이상의 score를 매칭으로 볼 것인가? 같은 threshold 설계도 애매한 문제로 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 커서 기반 페이지네이션을 사용해야 하는 이유에 대해 서술하였는데, RDB에서도 마찬가지지만 커서 기반 방식은 아래와 같이 ORDER BY를 쓰는 컬럼이 커서 컬럼이어야 합니다. &lt;code&gt;ORDER BY name&lt;/code&gt; 과 같은 다른 기준으로 정렬을 바꾼다면 별도의 데이터가 표시되고 즉 커서는 정렬 키와 1:1로 항상 묶여야 합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM table
WHERE created_at &amp;gt; :last_created_at
ORDER BY created_at ASC
LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 개념으로 벡터 DB도 Cosine 유사도 기반으로 정렬된 데이터를 다시 name 기준 정렬을 얹어야하는거죠. 그럼 페이징은 유사도 순으로 되어있는데 정렬을 바꾸면 유사도 순으로 볼 수 없기에 더 이상 벡터 검색 기능의 의미가 사라지는겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vector DB는 벡터 유사도 기준으로 상위 결과를 빠르게 찾고 보여주는 데 최적화된 도구이고, 전체 데이터에 대해 가격&amp;middot;날짜 등으로 완전히 정렬된 리스트를 보장하는 도구는 아닌걸로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무 생각도 없이 전부 되겠지? 생각을 했었는데 만능인 도구는 아닌게 Vector DB라고 생각되고, 만약 강력한 검색 기능이 필요하다면 통합 검색과 유사도 검색 두 가지를 제공하는 방식도 충분히 가능해보입니다. OpenSearch나 ElasticSearch처럼 검색엔진을 활용하는 하이브리드 방식을 추후에 생각해봐도 좋을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTGRYv/dJMcagjM2Rd/4ZdEyk6K9QppCkeGqG8XwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTGRYv/dJMcagjM2Rd/4ZdEyk6K9QppCkeGqG8XwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTGRYv/dJMcagjM2Rd/4ZdEyk6K9QppCkeGqG8XwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTGRYv%2FdJMcagjM2Rd%2F4ZdEyk6K9QppCkeGqG8XwK%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;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>DB/Vector</category>
      <category>3D 검색</category>
      <category>Object 검색</category>
      <category>qdrant</category>
      <category>벡터 데이터베이스</category>
      <category>벡터 디비</category>
      <category>에셋 검색</category>
      <category>파일 검색</category>
      <author>tomato_dev</author>
      <guid isPermaLink="true">https://mntdev.tistory.com/152</guid>
      <comments>https://mntdev.tistory.com/152#entry152comment</comments>
      <pubDate>Mon, 22 Dec 2025 19:32:15 +0900</pubDate>
    </item>
    <item>
      <title>[OpenAI - Codex] ChatGPT Cli 사용법, 오류 해결방법</title>
      <link>https://mntdev.tistory.com/150</link>
      <description>&lt;h3 style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size23&quot;&gt;Codex란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈AI&amp;nbsp;코덱스는&amp;nbsp;오픈AI가&amp;nbsp;개발한&amp;nbsp;인공지능&amp;nbsp;모델이다.&amp;nbsp;응답&amp;nbsp;시&amp;nbsp;자연어의&amp;nbsp;구문을&amp;nbsp;분석하고&amp;nbsp;코드를&amp;nbsp;생성한다.&amp;nbsp;비주얼&amp;nbsp;스튜디오&amp;nbsp;코드와&amp;nbsp;Neovim&amp;nbsp;등의&amp;nbsp;선별된&amp;nbsp;통합&amp;nbsp;개발&amp;nbsp;환경을&amp;nbsp;위한&amp;nbsp;프로그래밍&amp;nbsp;자동&amp;nbsp;완성&amp;nbsp;도구인&amp;nbsp;깃허브&amp;nbsp;코파일럿을&amp;nbsp;지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 GPT 사촌같은 녀석이다. 웹에서 비동기 에이전트 역할을 하기도 하고 Cli로도 사용이 가능하다.&lt;/p&gt;
&lt;h3 style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size23&quot;&gt;Codex Cli(ChatGPT Cli) 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1753753117517&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# npm
npm i -g @openai/codex
# 또는 Homebrew
brew upgrade codex&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size23&quot;&gt;codex 시작하기&lt;/h3&gt;
&lt;pre id=&quot;code_1753752485855&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#로그인(OpenAI Pro, Plus에 가입되어 있거나 API KEY 발급 필수)
codex login

#시작
codex

#즉시 명령
codex &quot;hi&quot;

#에이전트 모드
codex --full-auto &quot;create the fanciest todo-list app&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size23&quot;&gt;오류 원인과 결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 시작 시 다양한 이유로 오류가 발생한다. 시작 시 가장 만나게 되는 오류 중 하나는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1753752247853&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BackgroundEvent(BackgroundEventEvent { message: &quot;stream error: unexpected status 400 Bad Request: {\n  \&quot;error\&quot;: {\n    \&quot;message\&quot;: \&quot;Your organization must be verified to generate reasoning summaries.    ││
│Please go to: https://platform.openai.com/settings/organization/general and click on Verify Organization. If you just verified, it can take up to 15 minutes for access to propagate.\&quot;,\n    \&quot;type\&quot;:        ││
│\&quot;invalid_request_error\&quot;,\n    \&quot;param\&quot;: \&quot;reasoning.summary\&quot;,\n    \&quot;code\&quot;: \&quot;unsupported_value\&quot;\n  }\n}; retrying 10/10 in 2.139s&amp;hellip;&quot; })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 해당 링크에 가서 조직 설정을 완료하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N91xI/btsPCjuUT30/JkCiWU4gC75wOnV5FzPufk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N91xI/btsPCjuUT30/JkCiWU4gC75wOnV5FzPufk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N91xI/btsPCjuUT30/JkCiWU4gC75wOnV5FzPufk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN91xI%2FbtsPCjuUT30%2FJkCiWU4gC75wOnV5FzPufk%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;1560&quot; height=&quot;498&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;1306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2ZLOO/btsPAXzFWES/z3wjY5mcxK0WKIpBzzsL6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2ZLOO/btsPAXzFWES/z3wjY5mcxK0WKIpBzzsL6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2ZLOO/btsPAXzFWES/z3wjY5mcxK0WKIpBzzsL6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2ZLOO%2FbtsPAXzFWES%2Fz3wjY5mcxK0WKIpBzzsL6K%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;844&quot; height=&quot;1306&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;1306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;이후에 신분증 앞, 뒷면을 촬영하고 안면도 정면, 좌우를 촬영하여 제출하여야 한다.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게까지 복잡해야하나 싶기는 하지만 GPT Plus나 Pro만 결제하고 GPT Cli(Codex Cli)를 사용할 수 있다는것에 의의를 둔다면 썩 나쁘지는 않은 과정이긴 한 것 같다. 아직 본격적으로 테스트해보지는 않았지만 직접 GPT &amp;lt;-&amp;gt; Code를 왔다갔다 하는 것 보다 Claude Code나 Cli처럼 Ide에서 (콘솔을 통해) 바로 프로젝트를 수정할 수 있다는건 꽤나 매력적인 것 같다. 하지만 클로드에서 가능한 MCP 지원 기능이나 관련 프레임워크 등 나온지 오래 됐지만 아직은 커뮤니티가 작은 GPT Cli(Codex)이기 때문에 누릴 수 있는 이점도 있다고 본다.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 많이 겪는 오류 중 하나다&lt;/p&gt;
&lt;pre id=&quot;code_1753752699907&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;event
BackgroundEvent(BackgroundEventEvent {
message: &quot;stream error: stream disconnected before completion: stream closed before response.completed; retrying 1/10 in 191ms&amp;hellip;&quot;
})&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;해당 이슈의 원인은 다양한데, 현재 OpenAI에서 제대로 된 오류 메시지를 매핑하지 않았기 때문에 저렇게 뭉뚱그려서 연결을 할 수 없는 문제만 발생한다.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;이럴 때 확인해야 하는 사항이 몇가지 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ChatGPT&amp;nbsp;프로모션&amp;nbsp;크레딧으로&amp;nbsp;로그인하려면&amp;nbsp;ChatGPT&amp;nbsp;Plus&amp;nbsp;또는&amp;nbsp;Pro&amp;nbsp;구독을&amp;nbsp;시작한&amp;nbsp;지&amp;nbsp;7일&amp;nbsp;이상&lt;/li&gt;
&lt;li&gt;API 조직에 기본 결제 방법이 설정되어 있어야 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엥? 난 GPT 계정이 있는데 저건 뭐야? 라고 생각하실텐데... 왠지 몰라도 OpenAI의 API 플랫폼에 가입 후 결제 방법(신용카드 등록)이 되어 있어야 사용이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이런 귀찮음을 메리트도 있으니 걱정말자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ChatGPT 유료 사용자 리워드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT로 Codex CLI에 로그인하는 Plus 사용자는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;5달러 상당의 API 크레딧을&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;, Pro 사용자는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;50달러 상당의 API 크레딧을&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;받을 수 있다. 하지만 모든 프로모션 크레딧은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;발급 후 30일 후에&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;만료되니 빠르게 사용해보자.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size23&quot;&gt;다양한 사용법&lt;/h3&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;span&gt;✨&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;입력 값&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;Codex가 해주는 일&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;codex &quot;Refactor the Dashboard component to React Hooks&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Codex&lt;/span&gt;는&lt;span&gt; &lt;/span&gt;클래스&lt;span&gt; &lt;/span&gt;구성&lt;span&gt; &lt;/span&gt;요소를&lt;span&gt; &lt;/span&gt;다시&lt;span&gt; &lt;/span&gt;작성하고&lt;span&gt; &lt;/span&gt;실행한&lt;span&gt;&amp;nbsp;npm test&lt;/span&gt;다음&lt;span&gt; &lt;/span&gt;차이점을&lt;span&gt; &lt;/span&gt;보여줍니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;2&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;codex &quot;Generate SQL migrations for adding a users table&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;ORM&lt;/span&gt;을&lt;span&gt; &lt;/span&gt;추론하고&lt;span&gt;, &lt;/span&gt;마이그레이션&lt;span&gt; &lt;/span&gt;파일을&lt;span&gt; &lt;/span&gt;생성하고&lt;span&gt;, &lt;/span&gt;샌드박스&lt;span&gt; DB&lt;/span&gt;에서&lt;span&gt; &lt;/span&gt;실행합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;codex &quot;Write unit tests for utils/date.ts&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;테스트를&lt;span&gt; &lt;/span&gt;생성하고&lt;span&gt; &lt;/span&gt;실행하며&lt;span&gt; &lt;/span&gt;통과할&lt;span&gt; &lt;/span&gt;때까지&lt;span&gt; &lt;/span&gt;반복합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;4&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;codex &quot;Bulk-rename *.jpeg -&amp;gt; *.jpg with git mv&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;안전하게&lt;span&gt; &lt;/span&gt;파일&lt;span&gt; &lt;/span&gt;이름을&lt;span&gt; &lt;/span&gt;바꾸고&lt;span&gt; &lt;/span&gt;가져오기&lt;span&gt;/&lt;/span&gt;사용법을&lt;span&gt; &lt;/span&gt;업데이트합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;5&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;codex &quot;Explain what this regex does: ^(?=.*[A-Z]).{8,}$&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;단계별&lt;span&gt; &lt;/span&gt;인간&lt;span&gt; &lt;/span&gt;설명을&lt;span&gt; &lt;/span&gt;출력합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;6&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;codex &quot;Carefully review this repo, and propose 3 high impact well-scoped PRs&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;현재&lt;span&gt; &lt;/span&gt;코드베이스에서&lt;span&gt; &lt;/span&gt;영향력&lt;span&gt; &lt;/span&gt;있는&lt;span&gt; PR&lt;/span&gt;을&lt;span&gt; &lt;/span&gt;제안합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;7&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;codex &quot;Look for vulnerabilities and create a security review report&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;보안&lt;span&gt; &lt;/span&gt;버그를&lt;span&gt; &lt;/span&gt;찾아&lt;span&gt; &lt;/span&gt;설명합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;이 외에도 상당히 많은 프롬프트를 이용할 수 있고 활용할 수 있어 보인다.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;user-select: auto !important;&quot; href=&quot;https://github.com/openai/codex&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/openai/codex&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753670591140&quot; style=&quot;user-select: auto !important;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - openai/codex: Lightweight coding agent that runs in your terminal&quot; data-og-description=&quot;Lightweight coding agent that runs in your terminal - openai/codex&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/openai/codex&quot; data-og-url=&quot;https://github.com/openai/codex&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gXjxi/hyZnct7uQE/58VXZPrjB6hVLNFKWKTdhk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/blgbbT/hyZndmfFk3/oXlKTEjBc2sCqOavFZoam0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a style=&quot;user-select: auto !important;&quot; href=&quot;https://github.com/openai/codex&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/openai/codex&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gXjxi/hyZnct7uQE/58VXZPrjB6hVLNFKWKTdhk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/blgbbT/hyZndmfFk3/oXlKTEjBc2sCqOavFZoam0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot; style=&quot;user-select: auto !important;&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - openai/codex: Lightweight coding agent that runs in your terminal&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;Lightweight coding agent that runs in your terminal - openai/codex&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Error/개발환경</category>
      <category>ChatGPT</category>
      <category>chatgpt cli</category>
      <category>Codex</category>
      <category>codex cli</category>
      <category>codex error</category>
      <category>codex 사용법</category>
      <category>codex란</category>
      <category>openai</category>
      <category>클로드 코드</category>
      <author>tomato_dev</author>
      <guid isPermaLink="true">https://mntdev.tistory.com/150</guid>
      <comments>https://mntdev.tistory.com/150#entry150comment</comments>
      <pubDate>Tue, 29 Jul 2025 10:44:03 +0900</pubDate>
    </item>
    <item>
      <title>[Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트﻿] 인프런 강의 후기, 강의 강추!!</title>
      <link>https://mntdev.tistory.com/149</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l82Gt/btsPop4M1hc/Rb1GIqK37wphN7h4HfdQrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l82Gt/btsPop4M1hc/Rb1GIqK37wphN7h4HfdQrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l82Gt/btsPop4M1hc/Rb1GIqK37wphN7h4HfdQrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl82Gt%2FbtsPop4M1hc%2FRb1GIqK37wphN7h4HfdQrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;0 0 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;의존성&amp;nbsp;주입을&amp;nbsp;통해&amp;nbsp;쉬운&amp;nbsp;테스트를&amp;nbsp;만드는&amp;nbsp;방법&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;우선 강의는 &lt;a href=&quot;https://inf.run/5VcPj&quot;&gt;Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트&lt;/a&gt; 이며, 해당 강의를 통해 학습한 내용을 정리해보았다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;강의 후기는 그동안 추상적인 개념으로만 알고 있던 내용을 정말 잘 정리할 수 있는 기회이자 진작에 들었으면 많은 시행착오를 겪지 않아도 될만한 내용들이 강의에 담겼다고 본다. 그리고 개인적으로 이 강의가 가장 훌륭한점은 전달력이다. 해당 강사분은 카카오 개발자시니 개발 실력은 말할것도 없고, 딕션이나 설명하고자 하는 내용들이 부담없이 머리에 잘 들어온다. 개인적으로는 내용도 좋지만 전달력이 가장 좋아서 6시간 20분의 강의를 겨우 2~3일만에 전부 들었던 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;테스트의 중요성&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;해당 강의에서는 기존 레이어드 아키텍처에서 출발하여 클린 아키텍처의 구현인 헥사고날까지 코드를 변경하며 테스트의 중요성에 대해 얘기한다. 개인적인 생각이지만 테스트의 큰 장점 중 하나는 리펙토링이라고 생각하는데 해당 강의에서는 아키텍처를 변경하며 일어나는 무수히 많은 리펙토링을 안정적으로 수행하는 과정을 통해 테스트의 장점을 볼 수 있었다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;강의에서 언급하듯 아주 간단한 프로젝트 (단순 CRUD)만 있는 경우 테스트가 없어도 상관없지만 조금만 복잡해져도 테스트가 필요하다고 말한다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7gq9X/btsPoSTczzD/hD0kChTEIvkHU3f1Wvgax0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7gq9X/btsPoSTczzD/hD0kChTEIvkHU3f1Wvgax0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7gq9X/btsPoSTczzD/hD0kChTEIvkHU3f1Wvgax0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7gq9X%2FbtsPoSTczzD%2FhD0kChTEIvkHU3f1Wvgax0%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;2414&quot; height=&quot;1134&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;하지만 너무 테스트에만 매몰되어도 안된다. 강의 자료 중 클린 아키텍처의 추상화는 어디까지 되어야 하는지에 대해 &amp;ldquo;정답은 없다.&amp;rdquo;고 그냥 적당한 지점을 찾으면 된다고 한다.&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;의존성 주입이란?&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;단순하게 보자면 객체가 다른 의존성을 갖는것을 막고 의존성을 약화시키기 위해서 객체 내부에서 new 생성자를 통해 다른 객체를 생성하지 않고, 외부에서 파라미터로 전달받는 방법이다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7YGpE/btsPqlTNVG8/4K9FhpOObxi72imUpPbgY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7YGpE/btsPqlTNVG8/4K9FhpOObxi72imUpPbgY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7YGpE/btsPqlTNVG8/4K9FhpOObxi72imUpPbgY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7YGpE%2FbtsPqlTNVG8%2F4K9FhpOObxi72imUpPbgY1%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;1920&quot; height=&quot;1086&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 new는 사실상 하드코딩이며, 해당 클래스가 변경이 되는 시점에 의존성이 있는 모든 클래스를 변경 해야하고, 이는 OCP에 위배되기도 한다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 의존성을 주입받고.. 또 Chef를 호출하는 객체에서 Bread, Meat, Lettuce 등을 직접 생성하지 않고 의존성을 주입받아서 전달하고, 또 반복하다보면 결국 특정 클래스에서는 의존성을 주입하기 위해 하드 코딩인 new를 통해 객체를 생성하게 된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Spring 프레임워크는 의존성 주입을 통해 개발자가 특정 클래스에서 직접 의존성을 관리하는 대신, 특정 규칙에 따라 Spring이 제어권을 가지고 의존성을 주입할 수 있도록 한다. 이러한 접근 방식은 코드의 유연성과 유지보수성을 향상시켜 특정 클래스에서 하드 코딩을 하지 않도록 도와준다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이게 왜 중요하냐면 클래스를 테스트하는 경우 내부에서 직접 객체를 생성하게 된다면 테스트하기 어려워진다는 문제가 발생하기 때문이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어서 랜덤한 값을 생성하는 클래스가 내가 테스트하려는 클래스 내부에서 사용하고 있고, 그것에 대한 값을 맞추려면 이는 리플렉션이나 다이나믹 프록시 같은 별도의 복잡한 패턴을 구현하는 방식이 아니라면 랜덤한 값을 맞출 수 없게되고 이는 테스트 코드를 작성하는데 아주 큰 벽이 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cX8vaj/btsPpXZW9tx/A3tkPYkE6znbFphWNfslv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cX8vaj/btsPpXZW9tx/A3tkPYkE6znbFphWNfslv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cX8vaj/btsPpXZW9tx/A3tkPYkE6znbFphWNfslv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcX8vaj%2FbtsPpXZW9tx%2FA3tkPYkE6znbFphWNfslv0%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;1920&quot; height=&quot;613&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;Repository와 JPA의 의존성을 끊자&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이제 본론으로 돌아가서 xxxService를 테스트할 때 가장 문제가 되는 부분은 JPA에 강하게 결합되어 테스트하기 어려움에 있습니다. 이를 해결하기 위해 `@SpringBootTest`를 사용하는 방법이 있지만 스프링 컨테이너를 띄우고 실제 DB(혹은 테스트용 H2)를 사용해야 하는데 이는 무겁고 오래 걸리는 등 FIRST (좋은 테스트 조건)를 위배한다고 볼 수 있다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;무작정 통합 테스트가 나빠! 는 아니지만, 핵심 비즈니스 로직을 간단하게 테스트하고 회귀버그를 방지하면서 리펙토링 하기 쉽도록 하려면 단위 테스트(스몰 테스트)는 필수적이며 가장 많은 부분을 차지해야 한다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해서 Repository를 JPA에 의존하는 방법은 지양해야 한다고 볼 수 있다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TAl8I/btsPpSLlfBp/tzwi21zgan7vcEzTM8R000/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TAl8I/btsPpSLlfBp/tzwi21zgan7vcEzTM8R000/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TAl8I/btsPpSLlfBp/tzwi21zgan7vcEzTM8R000/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTAl8I%2FbtsPpSLlfBp%2Ftzwi21zgan7vcEzTM8R000%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;426&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;결국 위 이미지에서 처럼 Serivice에서 직접 JPARepository를 의존하지 않고, 필수적인 API만 명시되어 있는 Repository 인터페이스를 생성하여 이를 의존하도록 만드는 방법이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;그 다음 Repository의 구현체에서 JPARepository를 의존하도록 만들게 된다면 Coupling은 낮아지고 SOLID 원칙을 지키는 좋은 아키텍처가 만들어진다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이제 이렇게 하게된다면 테스트 코드를 작성할 때 ModelRepository 인터페이스를 상속한 FakeModelRepository를 만들어서 테스트에 사용하게 된다면 위에서 언급한 DB가 없이도 테스트가 가능한 코드가 만들어진다. 외부 의존성이 아예 없기 때문에 빠르고, 계속해서 테스트를 돌리면서 코드를 수정할 수 있기 때문에 배포에 대한 두려움도 사라지게 되는 장점도 있다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;Controller랑 Repository는 테스트 안하나요?&lt;/h2&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1014&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c182Sc/btsPoW8Rvgi/jTXhBhfPcXg04P1nVbKhH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c182Sc/btsPoW8Rvgi/jTXhBhfPcXg04P1nVbKhH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c182Sc/btsPoW8Rvgi/jTXhBhfPcXg04P1nVbKhH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc182Sc%2FbtsPoW8Rvgi%2FjTXhBhfPcXg04P1nVbKhH1%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;1920&quot; height=&quot;1014&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1014&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;해당 강의에서는 프레임워크와 라이브러리를 믿고 간다고 한다. 물론 이게 정확하지 않을 수 있지만 나의 생각에도 개발자 개인이 하는 것 보다, 해당 조직에서 더 정확하게 테스트할거라고 생각한다. 그렇기 때문에 우리가 만든 코드인 서비스와 도메인 정도까지만 테스트하는것이 맞다고 본다. (현재 프로젝트에서는 DTO까지 테스트 했다.)&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1014&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciWDV9/btsPqzRZraF/QmjjVkYxpb3WSmfBLYqjbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciWDV9/btsPqzRZraF/QmjjVkYxpb3WSmfBLYqjbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciWDV9/btsPqzRZraF/QmjjVkYxpb3WSmfBLYqjbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciWDV9%2FbtsPqzRZraF%2FQmjjVkYxpb3WSmfBLYqjbk%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;1920&quot; height=&quot;1014&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1014&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;결론(어디까지 의존성 주입을 해야할까)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPHJvP/btsPn5ZJuGJ/GU8oiodLho7KQjmKtHDABK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPHJvP/btsPn5ZJuGJ/GU8oiodLho7KQjmKtHDABK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPHJvP/btsPn5ZJuGJ/GU8oiodLho7KQjmKtHDABK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPHJvP%2FbtsPn5ZJuGJ%2FGU8oiodLho7KQjmKtHDABK%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;1920&quot; height=&quot;818&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이 방법의 가장 큰 장점은 JPA Entity와 Domain Entity를 나누었을 때 발휘한다고 강의에서 설명하지만 현재 Domain이 아주 작은 상태이기 때문에 현재 상태를 유지하면서 추후 서비스가 커진다면 Controller에서 Service를 직접 의존하지 않도록 변경하고, 순환 참조를 막기 위해서 DTO 객체를 계층 별 생성하면서 Entity를 JPA와 Domain으로 별도로 나누게 된다면 헥사고날 패턴으로 구현이 가능하겠지만 이전 단계에서도 꽤나 훌륭한 아키텍처라고 생각된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;강의와 예시 코드를 통해 기존에 생각도 못해봤던 테스트 범위나 효과에 대해서 알게 되었던 것 같습니다. 강의는 길었으며 깨달은 내용도 많았지만 가장 중요한건 딱 두가지라고 생각한다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;1.테스트가 어렵다면 코드를 변경해야하는 신호이며, 의존성 역전과 아키텍처 변경을 통해 해결이 가능하다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;2.무작정 오버 엔지니어링을 하지말고, 서비스의 규모와 개발자 리소스에 적절하게 아키텍처를 설계하자.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;만약 프로젝트가 복잡해져서 아키텍처 변경이 필요하다고 생각되는 경우, 각 계층을 분리하고 의존성을 단방향으로 만들기 위해 현재 Entity를 Domain Entity와 DB Entity로 변경하면서 그외에 CQRS 패턴을 적용하고, ~~Service 보다 ~~Reader, ~~Writer로 인터페이스를 나누어 구현하는 방향으로 간다면 커진 서비스를 테스트하고 운영하는데 많은 도움이 될 것으로 보인다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;1066&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boIPoQ/btsPpdQlXxg/cN0YHKQwkxk2k1JCiP8H4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boIPoQ/btsPpdQlXxg/cN0YHKQwkxk2k1JCiP8H4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boIPoQ/btsPpdQlXxg/cN0YHKQwkxk2k1JCiP8H4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboIPoQ%2FbtsPpdQlXxg%2FcN0YHKQwkxk2k1JCiP8H4K%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;1722&quot; height=&quot;1066&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;1066&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;이 외에 기본적인 테스트 방법(유닛 테스트, 통합 테스트, 컨트롤러, 서비스 , 레포지토리 테스트 전부 다 나옴)을 소개하면서 점진적으로 빌드업을 쌓아가면서 마지막에 이 개념들을 묶어서 설명하고 이 빌드업의 끝판왕인 클린 아키텍처와 그 구현인 헥사고날 아키텍처에 대해 아주아주 쉽게 설명한다.&amp;nbsp;&lt;/div&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;개인적으로 헥사고날에 안좋은 기억이 있는데, 이 강의를 보면서 다시 생각하게 됐다.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 링크 &lt;a href=&quot;https://inf.run/5VcPj&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://inf.run/5VcPj&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1752824440300&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트| 김우근 - 인프런 강의&quot; data-og-description=&quot;현재 평점 4.9점 수강생 2155명인 강의를 만나보세요. Spring에 테스트를 넣는 방법을 알려드립니다! 더 나아가 자연스러운 테스트를 할 수 있게 스프링 설계를 변경하는 방법을 배웁니다. Spring에 &quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://inf.run/5VcPj&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%98%A4%EB%8B%B5%EB%85%B8%ED%8A%B8&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HEsF3/hyZnjE6F9b/IXKeCqFyv18TCnrWHKJKrK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bQmPVn/hyZjcndLYB/1E7fxF5AKmaIbl7kfxasM1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/brFW7U/hyZnl30C1C/JxY0mknAdOxGY4FhIs8jpK/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://inf.run/5VcPj&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://inf.run/5VcPj&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HEsF3/hyZnjE6F9b/IXKeCqFyv18TCnrWHKJKrK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bQmPVn/hyZjcndLYB/1E7fxF5AKmaIbl7kfxasM1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/brFW7U/hyZnl30C1C/JxY0mknAdOxGY4FhIs8jpK/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트| 김우근 - 인프런 강의&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;현재 평점 4.9점 수강생 2155명인 강의를 만나보세요. Spring에 테스트를 넣는 방법을 알려드립니다! 더 나아가 자연스러운 테스트를 할 수 있게 스프링 설계를 변경하는 방법을 배웁니다. Spring에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Languege/Java &amp;amp; Spring</category>
      <category>Java</category>
      <category>JUnit5</category>
      <category>spring</category>
      <category>spring boot</category>
      <category>tdd 강의</category>
      <category>test</category>
      <category>Test Code</category>
      <category>인프런</category>
      <category>테스트 강의</category>
      <category>헥사고날 강의</category>
      <author>tomato_dev</author>
      <guid isPermaLink="true">https://mntdev.tistory.com/149</guid>
      <comments>https://mntdev.tistory.com/149#entry149comment</comments>
      <pubDate>Fri, 18 Jul 2025 16:42:58 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Java Validation은 어떻게 한글 메시지가 나올까? 배포 중 장애 발생</title>
      <link>https://mntdev.tistory.com/148</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트에서 @NotBlank 등 Bean Validation 어노테이션을 사용할 때, 로컬 환경에서는 한글 에러 메시지가 잘 나오지만, 배포(예: Jenkins &amp;rarr; AWS EC2) 환경에서는 갑자기 영어 메시지(must not be blank)가 출력되어 당황하신 경험이 있으실 겁니다. 이 글에서는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜 로컬과 배포 환경에서 메시지가 다르게 나오는지&lt;/li&gt;
&lt;li&gt;어떻게 한글 메시지를 항상 보장할 수 있는지를 차근차근 살펴보겠습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 문제 상황 재현&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;개발자 PC(macOS, OS 로케일 = ko_KR)&lt;/li&gt;
&lt;li&gt;AWS EC2 인스턴스(기본 OS 로케일 = en_US)&lt;/li&gt;
&lt;li&gt;코드에는 어노테이션에 메시지를 따로 지정하지 않음&lt;/li&gt;
&lt;li&gt;배포 파이프라인에서 테스트 코드에 오류 발생&lt;/li&gt;
&lt;li&gt;배포가 안되는 오류 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬과 배포 환경에서 다른 메시지가 노출되며, 특히 CI/CD 파이프라인 테스트가 깨지는 현상이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 git을 잘못 올려서 발생한 문제로 착각해서 캐시를 지우는 등 많은 삽질을 했습니다 ㅎ..&lt;/p&gt;
&lt;pre id=&quot;code_1752747171512&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MyDTO {
	@NotBlnak //아무런 메시지 설정 X
	private String name;
    ...
}


@Test
@DisplayName(&quot;DTO 유효성 검증 - 이름 누락&quot;)
void test() {
    // Given
    MyDTO request = MyDTO.builder()
            .name(null)
            .description(&quot;Desc&quot;)
            .build();

    // When
    ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
    Validator validator = validatorFactory.getValidator();
    Set&amp;lt;ConstraintViolation&amp;lt;ModelSaveRequest&amp;gt;&amp;gt; violations = validator.validate(request);

    // Then
    assertThat(violations).isNotEmpty();

    //여기서 오류 발생!!!
    assertThat(violations).anyMatch(v -&amp;gt; v.getPropertyPath().toString().equals(&quot;name&quot;) &amp;amp;&amp;amp; v.getMessage().equals(&quot;공백일 수 없습니다&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 원인 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 JVM 기본 로케일(Locale.getDefault())&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로컬(macOS)&lt;/b&gt;: OS 로케일이 ko_KR &amp;rarr; JVM 기본 로케일도 ko_KR&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EC2 인스턴스(리눅스)&lt;/b&gt;: 보통 en_US 로 설치 &amp;rarr; JVM 기본 로케일도 en_US&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;# 로케일 확인
$ locale
LANG=ko_KR.UTF-8        # macOS 예시
LC_ALL=
&amp;hellip;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;$ locale
LANG=en_US.UTF-8        # EC2 예시
LC_ALL=
&amp;hellip;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw6a4S/btsPnsGhX7Q/xGCc7PYtAxQDNjHP9zHot0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw6a4S/btsPnsGhX7Q/xGCc7PYtAxQDNjHP9zHot0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw6a4S/btsPnsGhX7Q/xGCc7PYtAxQDNjHP9zHot0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw6a4S%2FbtsPnsGhX7Q%2FxGCc7PYtAxQDNjHP9zHot0%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;1652&quot; height=&quot;958&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Bean Validation 메시지 번들 우선순위&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;ValidationMessages_{locale}.properties&lt;/b&gt; (프로젝트 classpath:/)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기본 내장 번들&lt;/b&gt; (hibernate-validator 라이브러리 제공 ValidationMessages.properties)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, ValidationMessages_ko.properties 가 없으면 내장 영어 번들(must not be blank)이 사용됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 해결 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 ValidationMessages_ko.properties 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 src/main/resources 폴더에 두 파일을 생성합니다.&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;ValidationMessages.properties&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;javax.validation.constraints.NotBlank.message=This field must not be blank&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ValidationMessages_ko.properties&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;javax.validation.constraints.NotBlank.message=값을 입력해주세요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 JVM 로케일이 ko_KR 일 때는 한글 메시지를, en_US 일 때는 영어 메시지를 각각 읽어옵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 스프링 MessageSource와 Validator 연동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring messages_*.properties 파일을 그대로 사용하려면, Validator 빈에 MessageSource 를 연결합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class ValidationConfig {
  @Bean
  public LocalValidatorFactoryBean validator(MessageSource messageSource) {
    LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
    // 스프링 메시지 소스를 우선 사용
    bean.setValidationMessageSource(messageSource);
    return bean;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;src/main/resources/messages_ko.properties 에도를 추가해 두면, 언제나 스프링 메시지 소스를 통해 한글 메시지를 가져옵니다.&lt;/li&gt;
&lt;li&gt;javax.validation.constraints.NotBlank.message=값을 입력해주세요&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 JVM 옵션으로 로케일 강제 고정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 스크립트나 Jenkins 빌드 설정에 JVM 옵션을 추가해, 애플리케이션이 무조건 한국어 로케일을 사용하도록 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;java \
  -Duser.language=ko \
  -Duser.country=KR \
  -jar myapp.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 파이프라인 내에서:&lt;/p&gt;
&lt;pre class=&quot;puppet&quot;&gt;&lt;code&gt;pipeline {
  environment {
    JAVA_OPTS = '-Duser.language=ko -Duser.country=KR'
  }
  stages {
    stage('Run') {
      steps {
        sh 'java $JAVA_OPTS -jar build/libs/app.jar'
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 환경과 배포 과정이 다를 수 있지만, 이런 메시지 하나로 오류가 날거라고 생각도 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 분들은 저처럼 이런 실수로 시간 낭비를 안하셨으면 좋겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Languege/Java &amp;amp; Spring</category>
      <author>tomato_dev</author>
      <guid isPermaLink="true">https://mntdev.tistory.com/148</guid>
      <comments>https://mntdev.tistory.com/148#entry148comment</comments>
      <pubDate>Thu, 17 Jul 2025 19:20:37 +0900</pubDate>
    </item>
    <item>
      <title>[Swagger] Spring, Spring Boot 호환되는 버전 정리</title>
      <link>https://mntdev.tistory.com/147</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjKxpW/btsO8IX0BEG/h6ybMQkYBO926JN4bfppl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjKxpW/btsO8IX0BEG/h6ybMQkYBO926JN4bfppl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjKxpW/btsO8IX0BEG/h6ybMQkYBO926JN4bfppl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjKxpW%2FbtsO8IX0BEG%2Fh6ybMQkYBO926JN4bfppl0%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;800&quot; height=&quot;461&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;springdoc-openapi &amp;times; Spring Boot 버전 불일치로 Swagger `/api-docs` 가 500을 뿜었던 기록&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 사내 &lt;b&gt;AI 서빙 모놀리식&lt;/b&gt;을 Spring Boot 3.x 로 올리면서&lt;br /&gt;Swagger-UI(&lt;code&gt;/swagger-ui&lt;/code&gt;), OpenAPI JSON(&lt;code&gt;/api-docs&lt;/code&gt;) 가 &lt;b&gt;500 Internal Server Error&lt;/b&gt; 를 내뿜는 대참사가 있었다.&lt;br /&gt;검색하면 흔히 나오는 &amp;ldquo;순환 참조&amp;rdquo;‧&amp;ldquo;Security 설정&amp;rdquo; 문제와는 달리, 이번엔 &lt;b&gt;버전 호환성&lt;/b&gt;이 주범이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 증상&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;요청&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;`GET /swagger-ui`&lt;/td&gt;
&lt;td&gt;UI는 뜨지만 &amp;ldquo;Failed to load API definition&amp;rdquo; 경고&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`GET /api-docs` (또는 `/v3/api-docs`)&lt;/td&gt;
&lt;td&gt;**500 Internal Server Error**&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔 로그에는 대략 이런 예외가 줄줄이 찍힌다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;java.lang.NoSuchMethodError:
  'void org.springframework.web.method.ControllerAdviceBean.&amp;lt;init&amp;gt;(java.lang.Object)'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;java.lang.NoClassDefFoundError:
  org/springdoc/core/models/GroupedOpenApi&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 원인 분석&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring Boot 3.3.x (Framework 6.2)&lt;/b&gt; 로 업그레이드&lt;/li&gt;
&lt;li&gt;기존에 쓰던 &lt;b&gt;&lt;code&gt;springdoc-openapi-starter-webmvc-ui 2.5.0&lt;/code&gt;&lt;/b&gt; 유지&lt;/li&gt;
&lt;li&gt;&amp;rarr; Boot 3.3 가 의존하는 Spring 6.2 에서 &lt;b&gt;&lt;code&gt;ControllerAdviceBean(Object)&lt;/code&gt; 생성자가 제거&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;springdoc 2.5.0 코드는 그 생성자를 호출 &amp;rarr; &lt;code&gt;NoSuchMethodError&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심&lt;/b&gt; : Spring Boot / Spring Framework 버전이 올라갈 때마다 springdoc 쪽도 &amp;ldquo;짝&amp;rdquo;이 맞는 버전으로 함께 올려야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결 과정 TL;DR&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;dependency tree&lt;/b&gt; 로 실제 런타임 버전 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;./gradlew :ai-playground:dependencies --configuration runtimeClasspath \ | grep -E &quot;spring-web[^a-zA-Z]|springdoc-openapi&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;버전 호환표&lt;/i&gt; 를 기준으로 Spring Boot &amp;harr; springdoc 버전을 &lt;b&gt;세트로 맞춤&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;./gradlew --refresh-dependencies clean build&lt;/code&gt; 후 재기동&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api-docs&lt;/code&gt; 200 OK &amp;rarr; Swagger-UI 정상 렌더링&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Spring Boot 2.7 ~ 3.5 호환 표  &lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spring Boot&lt;/th&gt;
&lt;th&gt;Spring Framework&lt;/th&gt;
&lt;th&gt;&lt;b&gt;공식 호환 springdoc&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;권장 GA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3.5.x&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6.4.x&lt;/td&gt;
&lt;td&gt;2.8.9 이상&lt;/td&gt;
&lt;td&gt;&lt;b&gt;2.8.9&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3.4.x&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6.3.x&lt;/td&gt;
&lt;td&gt;2.7 &amp;ndash; 2.8.x&lt;/td&gt;
&lt;td&gt;&lt;b&gt;2.8.9&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3.3.x&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6.2.x&lt;/td&gt;
&lt;td&gt;&lt;b&gt;2.6.x&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;2.6.4&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3.2.x (LTS)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6.1.x&lt;/td&gt;
&lt;td&gt;2.3 &amp;ndash; 2.5.x&lt;/td&gt;
&lt;td&gt;&lt;b&gt;2.5.0&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3.1.x&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6.0.x&lt;/td&gt;
&lt;td&gt;2.2.x&lt;/td&gt;
&lt;td&gt;&lt;b&gt;2.2.2&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3.0.x&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6.0.x&lt;/td&gt;
&lt;td&gt;2.0.x 이상&lt;/td&gt;
&lt;td&gt;&lt;b&gt;2.2.2&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2.7.x&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;5.3.x&lt;/td&gt;
&lt;td&gt;&lt;b&gt;1.6.11 이상 1.x&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;1.8.0&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실전 적용 예시 (Boot 3.2.x + springdoc 2.5.0)&lt;/h2&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// settings.gradle.kts
pluginManagement {
    plugins {
        id(&quot;org.springframework.boot&quot;) version &quot;3.2.9&quot;
        id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.7&quot;
    }
    repositories { gradlePluginPortal(); mavenCentral() }
}

// build.gradle.kts
dependencies {
    implementation(&quot;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;모듈별 build.gradle.kts 에 Boot 버전을 따로 적지 말 것!&lt;/i&gt;&lt;br /&gt;pluginManagement 쪽 버전이 자동 전파된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 마무리 &amp;mdash; 교훈&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring Boot &amp;harr; springdoc 버전 호환표&lt;/b&gt;를 항상 같이 확인하자.&lt;/li&gt;
&lt;li&gt;버전 업이 필요할 땐 &lt;b&gt;의존성 트리부터&lt;/b&gt; 찍어 보고,&lt;br /&gt;겹치는 라이브러리(특히 springdoc 1.x vs 2.x) 가 없는지 검증하자.&lt;/li&gt;
&lt;li&gt;SNAPSHOT/RC 에 의존해야 할 땐 CI 캐시 + 사내 Nexus 핀업이 필수!&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 Swagger가 다시 살아났고, 빌드/배포 파이프라인에도 버전 검증 스텝을 추가했다.&lt;/p&gt;</description>
      <category>Languege/Java &amp;amp; Spring</category>
      <category>Error</category>
      <category>Exception</category>
      <category>spring boot 3 swagger</category>
      <category>spring boot 3.5</category>
      <category>spring boot 3.5.2 swagger</category>
      <category>Swagger</category>
      <category>swagger springboot 3.5</category>
      <category>swagger 설정 오류</category>
      <category>스웨거</category>
      <category>오류</category>
      <author>tomato_dev</author>
      <guid isPermaLink="true">https://mntdev.tistory.com/147</guid>
      <comments>https://mntdev.tistory.com/147#entry147comment</comments>
      <pubDate>Tue, 8 Jul 2025 19:52:47 +0900</pubDate>
    </item>
    <item>
      <title>[ORACLE] 대규모 데이터 마이그레이션 도전</title>
      <link>https://mntdev.tistory.com/146</link>
      <description>&lt;p&gt;제목 : [ORACLE] 대규모 데이터 마이그레이션에서 인덱스 비활성화가 가져다주는 이점&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;⚠️ 운영 환경에서 대규모 마이그레이션 시 발생하는 문제&lt;/h2&gt;
&lt;p&gt;Oracle DB에서 기존 마켓 테이블의 이미지 여러 컬럼(IMAGE_URL1 ~ 4)로 구성된 데이터를 No 체계로 별도의 테이블로 관리하고 마켓 테이블에서는 IMAGE_NO를 마이그레이션하여 별도의 이미지 관리 테이블에서 URL을 관리하도록 구조 변경을 진행할 때 대규모 UPSERT 작업이 필요했습니다.&lt;br&gt;처음 배치를 통해 점진적 마이그레이션을 진행하려 했지만, 내부 의견으로는 &lt;code&gt;번거로우니 서비스 중단 후 한번에 작업하자&lt;/code&gt; 로 결정되어 빅뱅 패치를 진행하게 되었습니다.&lt;br&gt;운영 환경에서 한 번에 모든 데이터를 마이그레이션하다 보니, 개발 환경과 비교해서 처리 속도가 급격히 느려지거나 시스템 자원(CPU, I/O, 메모리) 경합이 발생하는 문제가 관찰되었습니다.&lt;/p&gt;
&lt;p&gt;&amp;quot;MVCC로 인해 전혀 영향이 없는 SELECT만 하는 쿼리가 몇개 실행되는 정도 인데도, 마이그레이션 작업이 이렇게까지 느려질 수 있나?&amp;quot; 하는 의문을 갖게 되었고, 자세히 살펴보니 다음과 같은 원인들을 발견할 수 있었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt; 문제 찾기&lt;/h2&gt;
&lt;p&gt;이슈로는 다음과 같은 현상이 있었어요:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;대규모 UPSERT/INSERT 작업 시 DB 서버 부하가 급격히 증가  &lt;/li&gt;
&lt;li&gt;기존 서비스에서 SELECT만 하고 있음에도, 전체 성능이 저하  &lt;/li&gt;
&lt;li&gt;인덱스가 걸린 컬럼에 대해서는 UPDATE/INSERT가 일어날 때마다 인덱스를 유지해야 하므로 트랜잭션 시간이 길어짐&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;특히, 한 번에 대규모로 데이터를 옮기다 보니 언두(Undo) 및 리두(Redo) 로그 생성량이 폭증하고, 인덱스를 유지하는 데 드는 비용이 기하급수적으로 커졌습니다. 그렇다면 대규모 DML 시 인덱스를 어떻게 다뤄야 할까요?&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;  인덱스 유지 비용과 문제 시나리오&lt;/h2&gt;
&lt;p&gt;Oracle은 MVCC(Multi-Version Concurrency Control)를 사용해 SELECT가 직접 블로킹을 일으키지는 않습니다. 그렇지만, 아래와 같은 시나리오에서 성능 저하가 가속화될 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;한 번에 너무 많은 데이터를 UPSERT  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;대량 트랜잭션으로 인해 Undo/Redo 로그가 급증  &lt;/li&gt;
&lt;li&gt;Redo 로그 스위치가 매우 자주 발생하여 I/O 부하 증가  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;여러 인덱스가 있는 테이블에 대량 Insert/Update  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;각각의 인덱스를 실시간으로 갱신해야 하기 때문에 추가 성능 부담  &lt;/li&gt;
&lt;li&gt;인덱스가 많을수록 작업 시간이 지연  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;서비스 쿼리 &amp;amp; 마이그레이션 쿼리 동시 사용  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;리소스(CPU, 메모리, I/O)에 대한 경합  &lt;/li&gt;
&lt;li&gt;언두 영역 부족 시 성능 저하 가속  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;결과적으로, 인덱스가 많은 테이블에 대량의 DML이 수행되면 각 인덱스를 모두 갱신해야 하므로 작업 시간이 크게 늘어납니다.  &lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;✅ 문제해결: 인덱스 비활성화 전략&lt;/h2&gt;
&lt;p&gt;이 문제를 해결하기 위한 핵심 아이디어 중 하나는, 마이그레이션 시점에 “불필요하게” 인덱스를 유지할 필요가 없다면 인덱스를 일시적으로 비활성화했다가, 작업이 완료된 후에 다시 활성화(재생성)하는 것입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;인덱스 비활성화 (UNUSABLE 혹은 DROP)  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“ALTER INDEX 인덱스명 UNUSABLE” 명령을 통해 인덱스를 사용할 수 없게 설정  &lt;/li&gt;
&lt;li&gt;혹은 “DROP INDEX 인덱스명”으로 아예 제거 후 마이그레이션이 끝나면 다시 CREATE INDEX  &lt;/li&gt;
&lt;li&gt;UNIQUE/PRIMARY KEY 인덱스, FK 제약이 있는 경우 주의(제약 비활성 → DML → 제약 재활성화)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;대량 DML 수행  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테이블에 대량 UPSERT/INSERT/UPDATE 작업을 진행  &lt;/li&gt;
&lt;li&gt;인덱스 유지 비용이 없으므로 트랜잭션 시간이 단축됨  &lt;/li&gt;
&lt;li&gt;Undo/Redo 처리량도 감소 (단, 데이터량이나 병렬처리 방식에 따라 차이가 있을 수 있음)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;인덱스 재생성(혹은 REBUILD)  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;작업 완료 후 “ALTER INDEX 인덱스명 REBUILD” 또는 “CREATE INDEX …”를 통해 인덱스를 다시 생성  &lt;/li&gt;
&lt;li&gt;병렬(PARALLEL) 옵션 활용 시 재생성 시간을 단축할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;  운영 환경에서의 영향&lt;/h2&gt;
&lt;p&gt;인덱스 비활성화 후 마이그레이션을 진행하면 다음과 같은 효과를 기대할 수 있습니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;성능 향상  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;대량 DML 시 인덱스 유지 비용이 제거되므로 트랜잭션 처리 속도가 빨라짐  &lt;/li&gt;
&lt;li&gt;DB I/O 부하 감소로 인해 전체 운영 서비스도 방해를 덜 받음  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;배치 작업 시간 단축  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;마이그레이션 작업량이 크더라도, 인덱스가 없는 상태에서는 데이터 적재 속도가 빨라 더 짧은 시간 내에 완료  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;장애(또는 에러) 발생 시 복구 부담 완화  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;부분적으로 작업을 나누어 진행하고, 각 배치 단위에 대해 커밋 관리가 쉬워짐  &lt;/li&gt;
&lt;li&gt;인덱스를 미리 Drop/Unusable 상태로 두면, 잘못된 인덱스 유지로 인한 에러 가능성 최소화  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;다만, 인덱스가 UNUSABLE 상태일 때 해당 인덱스를 사용하는 SELECT 쿼리가 있으면 오류가 발생할 수 있으니, 운영 중이라면 쿼리 패턴 분석, 장애 시간 고려가 필수입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;  권장 사항&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;배치 단위로 작업 분할  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한꺼번에 모든 행을 처리하기보다는 적절한 범위(예: 몇 만 건 단위)로 나누어 작업  &lt;/li&gt;
&lt;li&gt;트랜잭션 크기가 줄어들어 언두 및 로그 부하 분산  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;인덱스 비활성화(드롭) 후 마이그레이션  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FK 및 UNIQUE 제약이 있는 경우 제약을 먼저 풀어야 하는지 사전 조사 필요  &lt;/li&gt;
&lt;li&gt;마이그레이션 완료 후 인덱스를 병렬로 재생성(Rebuild)  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;서비스 영향 최소화  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;리소스 사용량이 적은 시간(야간 등)에 배치 작업 실행  &lt;/li&gt;
&lt;li&gt;필요 시 리소스 제한을 걸어 병렬도(Parallel) 조절  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;작업 후 모니터링 및 튜닝  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AWR 리포트, V$SESSION, V$SQL 등의 성능 지표 확인  &lt;/li&gt;
&lt;li&gt;인덱스 재생성 후 정상적으로 SELECT에 활용되고 있는지 확인  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;  참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/database/&quot;&gt;Oracle Database 공식 문서&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;[Effective Oracle Database 10g Design &amp;amp; Programming - Thomas Kyte]  &lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/&quot;&gt;Oracle Index 사용 가이드: DROP vs UNUSABLE vs REBUILD&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt; 결론&lt;/h2&gt;
&lt;p&gt;운영 환경에서 대규모 마이그레이션을 진행할 때, 대량의 DML 연산에 의해 인덱스 유지 비용이 급격히 증가하는 것은 흔히 겪는 문제입니다. 특히 테이블에 여러 인덱스가 구성되어 있을수록 부담이 커집니다.&lt;/p&gt;
&lt;p&gt;이를 해결하는 실질적인 방법은 “필요 없는 인덱스를 잠시 끄고(UNUSABLE, DROP) 나중에 재생성”하는 접근입니다. 이로써 마이그레이션 속도를 크게 높이고, 서비스 영향도 줄일 수 있습니다. 다만 운영 중 사용 중인 인덱스라면 충분한 사전 검토 및 작업 스케줄링을 통해 안정적으로 진행해야 하며, FK/UNIQUE와 같은 제약 사항은 미리 해제와 재설정을 고려해야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Tip: 대량 마이그레이션에서 인덱스 비용을 최소화하는 것은 마이그레이션 시간, 장애 발생 리스크 모두를 줄이는 핵심 전략입니다. 사전에 이런 방법을 알았다면, 조금 더 문제가 없도록 처리가 가능했을 것 같아요.   &lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category>Error/트러블슈팅</category>
      <author>tomato_dev</author>
      <guid isPermaLink="true">https://mntdev.tistory.com/146</guid>
      <comments>https://mntdev.tistory.com/146#entry146comment</comments>
      <pubDate>Thu, 17 Apr 2025 07:48:24 +0900</pubDate>
    </item>
    <item>
      <title>[DDD-Quickly] 5,6장 요약 정리 - 모델 무결성과 오늘날 DDD</title>
      <link>https://mntdev.tistory.com/145</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MEJaN/btsMUqKtuUB/UWL2KxMFsSdrVO0jYnkK80/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MEJaN/btsMUqKtuUB/UWL2KxMFsSdrVO0jYnkK80/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MEJaN/btsMUqKtuUB/UWL2KxMFsSdrVO0jYnkK80/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMEJaN%2FbtsMUqKtuUB%2FUWL2KxMFsSdrVO0jYnkK80%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;411&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 도메인 주도 설계(DDD) 책의 5장과 6장에 대해 정리하고, 그 과정에서 얻은 인사이트를 공유해 보려 합니다. 5장은 대규모 프로젝트에서 모델 무결성(내부 일관성, 통일성)을 지키기 위한 전략적 패턴과 기법을, 6장은 오늘날 DDD가 왜 더욱 중요해졌는지를 다룹니다.&lt;br /&gt;두 장 모두 한층 더 &amp;ldquo;전략적인 관점&amp;rdquo;에서 DDD를 바라볼 수 있어서 개인적으로도 도움이 많이 되었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;분할된 컨텍스트 (Bounded Context)&lt;br /&gt;1) 컨텍스트의 개념
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;용어가 특정 의미를 갖도록 보장되는 모델의 범위&lt;/b&gt;&lt;/span&gt;&amp;rdquo;를 말합니다.&lt;/li&gt;
&lt;li&gt;대규모 기업 시스템에서는 전체를 하나로 묶으려 하기보다, 서로 자연스럽게 묶이는 개념을 중심으로 모델을 쪼개어 관리하는 편이 모델 무결성 유지에 유리합니다.&lt;/li&gt;
&lt;/ul&gt;
2) 분할된 컨텍스트 &amp;ne; 모듈
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨텍스트는 &amp;ldquo;논리적 프레임&amp;rdquo;으로, 해당 범위 안에서 일관된 도메인 언어와 모델을 운영합니다.&lt;/li&gt;
&lt;li&gt;모듈은 모델 내부를 조직화하는 방법으로, 코드 차원의 구조화(패키지&amp;middot;폴더 등)에 가깝습니다.&lt;/li&gt;
&lt;/ul&gt;
&amp;rarr; 예시: 전자상거래 시스템에서 &amp;ldquo;구매/주문 관리&amp;rdquo;와 &amp;ldquo;리포팅/메시징&amp;rdquo;을 별도의 모델(컨텍스트)로 분리해, 팀별로 독립적으로 작업하면서도 필요한 인터페이스만 딱 맞게 정의해 상호작용하게 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;지속적인 통합 (Continuous Integration)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 팀이 동일한 컨텍스트에서 개발할 경우, 모델이 제각각 흩어지기 쉽습니다.&lt;/li&gt;
&lt;li&gt;이를 방지하려면 자동화된 빌드&amp;middot;테스트 체계를 적용하고, 모델과 코드를 수시로 통합하는 습관을 들여야 합니다.&lt;/li&gt;
&lt;li&gt;분할된 컨텍스트 간에도 &amp;ldquo;정기적으로&amp;rdquo; 혹은 &amp;ldquo;사건이 있을 때&amp;rdquo; 상호작용 규칙이 올바른지 점검해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;컨텍스트 맵 (Context Map)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 애플리케이션에서 여러 컨텍스트와 그 관계를 한눈에 볼 수 있도록 표현한 &amp;ldquo;전체 지도&amp;rdquo;&lt;/li&gt;
&lt;li&gt;각 컨텍스트의 경계와 의존관계를 명시적으로 표현해, 프로젝트 전 구성원이 공유하고 이해하도록 합니다.&lt;/li&gt;
&lt;li&gt;중복 범위, 예상치 못한 충돌 지점을 발견하고 조정하는 데 큰 도움이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;컨텍스트 간 통합을 위한 주요 패턴들&lt;br /&gt;1) 공유 커널 (Shared Kernel)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 팀이 밀접하게 협력해야 할 때, &amp;ldquo;공동으로 사용해야 할 모델 일부&amp;rdquo;를 별도 모듈로 추출해 공유합니다.&lt;/li&gt;
&lt;li&gt;이 공유 부분은 양쪽 팀 모두 임의 변경 불가하며, 서로 의사소통하여 일관되게 진화시켜야 합니다.&lt;/li&gt;
&lt;/ul&gt;
2) 고객-공급자 (Customer-Supplier)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 컨텍스트(공급자)가 다른 컨텍스트(고객)에게 필요한 기능&amp;middot;정보를 제공하는 관계&lt;/li&gt;
&lt;li&gt;공유 커널 없이, 공급자가 제공하는 인터페이스나 DB 스키마를 고객이 소비하는 구조입니다.&lt;/li&gt;
&lt;li&gt;고객 컨텍스트가 &amp;ldquo;요구사항&amp;rdquo;을 공급자에게 제시해 협의하고, 공급자는 그 규약을 지키는 방식으로 협업합니다.&lt;/li&gt;
&lt;/ul&gt;
3) 변질 방지 레이어 (Anti-Corruption Layer)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레거시나 외부 시스템과 상호작용할 때, &amp;ldquo;그쪽 모델을 그대로 받아들여 내 도메인을 오염시키지 않기&amp;rdquo; 위해 중간 계층을 둡니다.&lt;/li&gt;
&lt;li&gt;번역(매핑) 기능을 통해 우리 쪽 모델은 &amp;ldquo;깨끗하고 일관되게&amp;rdquo; 유지하고, 외부 모델과의 충돌이나 영향은 레이어가 책임집니다.&lt;/li&gt;
&lt;/ul&gt;
4) 분할 방식 (Separate Ways)
&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;
5) 오픈 호스트 서비스 (Open Host Service)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 외부에서 내 시스템을 이용해야 할 때, 공용 프로토콜/서비스를 정의해 &amp;ldquo;공개&amp;rdquo;합니다.&lt;/li&gt;
&lt;li&gt;번역 레이어마다 따로 구현할 필요 없이 합의된 API나 프로토콜을 사용하는 식으로, 모든 공급자/소비자 간 통합을 단순화할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;증류 (Distillation)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인이 큰 경우 핵심 영역(Core Domain)과 일반 서브 도메인을 구분해, 핵심에 집중하고 일반적인 부분은 별도 솔루션&amp;middot;아웃소싱 등을 고려합니다.&lt;/li&gt;
&lt;li&gt;제한된 자원을 핵심에만 몰아주어 설계 역량을 집중하고, 비핵심에는 적절한 수단을 택하는 전략이 중요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘날 DDD가 더욱 중요한 이유 (6장)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에릭 에반스의 인터뷰 요약을 통해, DDD의 필요성이 과거보다 더 커졌음을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 기술 플랫폼 발전&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바, .NET, 루비 등의 웹/클라우드/마이크로서비스 플랫폼이 성숙해지면서, &amp;ldquo;도메인 모델을 유지하기에 좋은 구조&amp;rdquo;를 실현하기가 훨씬 수월해졌습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&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;애자일 프레임워크나 DevOps 문화가 퍼지면서, 도메인 전문가와 개발자가 밀접하게 협력하고 빠르게 피드백을 주고받는 환경이 조성되었습니다.&lt;/li&gt;
&lt;li&gt;DDD가 강조하는 상시적 대화, 지속적 통합, 모델 개선이 훨씬 자연스러워진 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 실무에서의 조언&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 도메인 모두에 DDD를 적용하려 애쓰기보다는, &amp;ldquo;핵심 도메인&amp;rdquo;을 잘 골라 DDD 기법을 집중 적용해 보는 전략이 좋습니다.&lt;/li&gt;
&lt;li&gt;모델 리팩터링 과정에서 다양한 실험과 실패를 통해, 원하는 수준의 모델 완성도를 높여 가는 것이 중요하다고 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✍️ 마무리 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;분할된 컨텍스트를 통해 대규모 모델을 여러 팀이 각각 유지&amp;middot;관리할 수 있게 하고, 컨텍스트 맵으로 전체 그림을 공유합니다.&lt;/li&gt;
&lt;li&gt;지속적인 통합, 공유 커널, 고객-공급자, 변질 방지 레이어, 분할 방식, 오픈 호스트 서비스 등 다양한 패턴을 활용해 &amp;ldquo;서로 다른 컨텍스트&amp;rdquo;가 함께 동작하도록 조정합니다.&lt;/li&gt;
&lt;li&gt;핵심 모델(핵심 도메인)에 집중 투자하고, 일반적이며 복잡도가 낮은 부분은 외부 솔루션&amp;middot;아웃소싱&amp;middot;단순화 방식을 검토해 효율성을 높입니다.&lt;/li&gt;
&lt;li&gt;오늘날 비즈니스의 요구사항이 정교해지고, 협업 환경과 인프라가 개선됨에 따라 DDD의 효과가 더욱 극대화되고 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책이 마무리되었는데 막연하게 DDD에 대해&amp;nbsp; 크게 어려운 개념은 아니라고 생각했습니다. 하지만 생각보다 난이도가 있는 내용이고 단순하게 개발 방법론이라기 보다는 조금 더 상위의 개념이라고 느꼈고, &lt;b&gt;DDD는 개발만 잘하면 되는 방법이 아닌, 여러 동료와 협업하면서 좋은 제품을 만들기 위한 지침&lt;/b&gt;이라고 생각됩니다. 잘 이해가 안됐던 부분들도 여러 번 읽다보니 완벽하게는 아니지만 개념적으로 이해된 것 같습니다. 실제로 프로젝트에 적용해보면서 핵심 도메인을 골라서 적용해보며 체감하고 추후에 다시 정리된 내용을 보는게 좋을 것 같다고 느꼈습니다.&lt;/p&gt;</description>
      <category>Study/Side Proejct</category>
      <category>Bounded Context</category>
      <category>context map</category>
      <category>ddd</category>
      <category>고객-공급자</category>
      <category>공유 커널</category>
      <category>도메인 주도 개발</category>
      <category>변질 방지 레이어</category>
      <category>분할된 컨텍스트</category>
      <category>컨텍스트</category>
      <category>컨텍스트 맵</category>
      <author>tomato_dev</author>
      <guid isPermaLink="true">https://mntdev.tistory.com/145</guid>
      <comments>https://mntdev.tistory.com/145#entry145comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:47:25 +0900</pubDate>
    </item>
    <item>
      <title>[DDD-Quickly] 3,4장 요약 정리 - 모델 주도 설계와 리팩터링</title>
      <link>https://mntdev.tistory.com/144</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daTDbD/btsMPxwYY4z/senkaIm1eyP8vwS5B6m0RK/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daTDbD/btsMPxwYY4z/senkaIm1eyP8vwS5B6m0RK/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daTDbD/btsMPxwYY4z/senkaIm1eyP8vwS5B6m0RK/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaTDbD%2FbtsMPxwYY4z%2FsenkaIm1eyP8vwS5B6m0RK%2Ftfile.dat&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;300&quot; height=&quot;411&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 도메인 주도 설계(DDD) 책의 3장과 4장에 대해 정리하고, 그 과정에서 얻은 인사이트를 공유해보려 합니다. DDD는 단순히 &amp;ldquo;도메인을 분석하고 모델링하는 것&amp;rdquo;을 넘어, 모델을 코드와 긴밀하게 연결해 나가는 일련의 과정이 핵심이라는 점이 인상 깊었습니다. 3장과 4장에서 평소에 가장 궁금했던 내용들에 대한 답이 많이 나온 것 같습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 모델 주도 설계(Model-Driven Design)란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 개발에서 &amp;ldquo;분석과 설계가 완전히 분리&amp;rdquo;되고, 비즈니스 전문가, 분석가, 개발자가 서로 동떨어진 방식으로 일하면 모델과 코드 간에 큰 괴리가 발생합니다.&lt;br /&gt;&amp;rarr; 이 괴리를 줄이기 위해 고안된 설계 기법이 바로 도메인 주도 설계(DDD)입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 왜 모델 주도 설계인가?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인의 핵심 개념을 담은 &amp;ldquo;모델&amp;rdquo;이 소프트웨어의 설계와 구현 전 과정에서 중심이 됩니다.&lt;/li&gt;
&lt;li&gt;모델을 기반으로 개발하면, &amp;ldquo;코드의 변경 = 모델의 변경&amp;rdquo;이라는 등식을 지킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;모델의 무결성(정확성)을 유지하려면, 모델을 잘 아는 사람이 실제 코드 구현도 책임져야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 모델 주도 설계 vs. 개발 툴과 언어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DDD는 자체가 특정 기술 스택&amp;middot;프레임워크를 강제하지 않습니다.&lt;/li&gt;
&lt;li&gt;다만, 객체지향 프로그래밍(OOP)처럼 &amp;ldquo;모델링 패러다임&amp;rdquo;을 지원하는 툴&amp;middot;언어가 더 적합합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 클래스, 상호 메시지, 인스턴스 등이 모델의 개념(엔티티, 값 객체, 연관)과 직접 매핑될 수 있으므로.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 분석 모델만으로는 부족하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석 단계에서 만든 &amp;ldquo;분석 모델&amp;rdquo;이 아무리 훌륭해도, 실제 코드 구현에 까다로우면 현장에서 쓸모가 떨어집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;엔티티의 영속성&amp;rdquo; 문제, 복잡한 객체 연관 등은 코드로 옮길 때 드러나는 실질적인 이슈입니다.&lt;/li&gt;
&lt;li&gt;분석 모델과 코드 설계를 별도의 담당자가 각각 진행하면 매핑 관계가 점차 사라질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;rarr; 해법: 모델과 코드 설계 과정을 끊임없이 왕복하며 개선하자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국, 도메인 전문가와 개발자가 서로의 영역에서만 일하지 않고, 모델을 공동으로 다듬고 코드에 반영하는 &amp;ldquo;모델 주도 설계&amp;rdquo;가 필수라는 결론에 도달하게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 모델 주도 설계를 위한 핵심 패턴들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD의 주요 설계 패턴에는 엔티티, 값 객체, 서비스, 모듈, 애그리게잇, 팩토리, 리파지토리 등이 있습니다.&lt;br /&gt;이를 어떻게 배치하고 계층화하느냐가 DDD 구현의 중요한 포인트가 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 계층형 아키텍처(Layered Architecture)&lt;/h3&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;UI/프레젠테이션 레이어: 사용자에게 정보 표시, 사용자 입력 수집&lt;/li&gt;
&lt;li&gt;인프라스트럭처 레이어: 데이터베이스, 네트워크, 메시지 전달 등 기술적인 부분&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 각 레이어가 &amp;ldquo;도메인&amp;rdquo;과 섞이지 않도록 분리하면, &amp;ldquo;도메인 로직&amp;rdquo;이 선명해지고 재사용성, 유지보수성이 높아집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 엔티티(Entity)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;식별자(Identifier)&amp;rdquo;로 구분되는 지속성 있는 객체&lt;/li&gt;
&lt;li&gt;라이프사이클 전반에 걸쳐 같은 식별자를 유지&lt;/li&gt;
&lt;li&gt;예: 주문(ORDER), 계좌(ACCOUNT), 회원(USER) 등이 대표적인 엔티티가 될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 값 객체(Value Object)&lt;/h3&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;불변(Immutable)으로 다루는 것이 권장됨&lt;/li&gt;
&lt;li&gt;예: 주소(Address), 좌표(Coordinates), 통화(Money) 등이 해당&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) 서비스(Service)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;도메인 개념을 객체로 매핑하기 어렵거나, 여러 엔티티에 걸친 행위&amp;rdquo;를 캡슐화&lt;/li&gt;
&lt;li&gt;상태를 가지지 않으며, 특정 연산만을 제공&lt;/li&gt;
&lt;li&gt;예: 여러 계좌 간 자금 이체, 결제 승인/취소 등&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5) 모듈(Module)&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;6) 집합(Aggregate)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모델 내 연관 관계가 복잡해질 경우, &amp;ldquo;하나의 통일된 무결성&amp;rdquo;을 적용하기 위해 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;루트 엔티티(Root)를 통해서만 내부 엔티티나 값 객체를 조작&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;불변식을 유지하는 데 큰 도움을 줌&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 깊은 통찰을 향한 리팩터링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 만든 모델은 항상 &amp;ldquo;거칠고 피상적&amp;rdquo;입니다. 도메인이 복잡하다면, 여러 번의 시행착오와 리팩터링을 거쳐야 비로소 안정되고 깊이 있는 모델이 완성됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 지속적인 리팩터링&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;2) 핵심 개념 드러내기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 코드 상에 &lt;b&gt;암시적으로 존재&lt;/b&gt;하던 개념을 찾아서 명확히 드러내는 과정&lt;/li&gt;
&lt;li&gt;각종 계산이나 절차가 복잡해 보일 때, &amp;ldquo;사실상 존재하는 핵심 개념&amp;rdquo;을 모델로 끌어내면 가독성이 크게 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) 책에서 든 간단한 예로, 책장을 구현하는 Bookshelf 코드가 있다고 해봅시다:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Bookshelf {
    private int capacity = 20;
    private Collection&amp;lt;Book&amp;gt; content;

    public void add(Book book) {
        if(isSpaceAvailable()) {
            content.add(book);
        } else {
            throw new IllegalArgumentException(&quot;The bookshelf has reached its limit.&quot;);
        }
    }

    private boolean isSpaceAvailable() {
        return content.size() &amp;lt; capacity;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 공간 제약(capacity) 같은 &amp;ldquo;불변식(invariant)&amp;rdquo;을 좀 더 명시적으로 드러낼 수 있습니다.&lt;/li&gt;
&lt;li&gt;이 과정에서 &lt;b&gt;isSpaceAvailable()&lt;/b&gt; 메서드는 &amp;ldquo;제약 조건&amp;rdquo;을 표현하는 로직이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 명세(Specification)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 객체가 &amp;ldquo;어떤 조건&amp;rdquo;을 만족하는지 여부를 판별하는 로직&lt;/li&gt;
&lt;li&gt;복잡한 비즈니스 규칙을 명세화된 객체 안에 캡슐화하여, 코드 중복을 제거하고 일관성을 높일 수 있습니다.&lt;/li&gt;
&lt;li&gt;큰 규칙을 여러 개의 작은 규칙(명세)로 분할, 이를 조합(AND, OR 등)해 가독성을 높이게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✍️ 마무리 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;ldquo;모델 주도 설계(Model-Driven Design)&amp;rdquo;는 모델이 단순한 UML이나 분석 산출물로 끝나는 게 아니라, 실제 코딩 단계까지 긴밀히 스며드는 설계 기법&lt;/li&gt;
&lt;li&gt;엔티티, 값 객체, 서비스, 모듈, 애그리게잇, 팩토리, 리파지토리 패턴을 통해 도메인의 구조와 라이프사이클을 명확히 표현할 수 있음&lt;/li&gt;
&lt;li&gt;모델과 코드는 서로 떨어질 수 없는 한 몸이며, 개발자는 모델을 아주 잘 이해해야 함&lt;/li&gt;
&lt;li&gt;초기 모델은 얕을 수밖에 없고, 현실 도메인이 복잡할수록 끊임없는 리팩터링과 협업을 통해 점점 정교해짐&lt;/li&gt;
&lt;li&gt;지속적인 리펙토링을 통해 안정화가 가능함&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Study/Side Proejct</category>
      <category>aggregate</category>
      <category>ddd</category>
      <category>MDD</category>
      <category>UML</category>
      <category>계층형 아키텍처</category>
      <category>도메인 주도 개발</category>
      <category>도메인 주도 설계</category>
      <category>레이어드 아키텍처</category>
      <category>어그리게이트</category>
      <category>집합</category>
      <author>tomato_dev</author>
      <guid isPermaLink="true">https://mntdev.tistory.com/144</guid>
      <comments>https://mntdev.tistory.com/144#entry144comment</comments>
      <pubDate>Wed, 19 Mar 2025 21:29:11 +0900</pubDate>
    </item>
    <item>
      <title>[DDD-Quickly] 1,2장 요약 정리 - DDD와 유비쿼터스 언어</title>
      <link>https://mntdev.tistory.com/143</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eZoZye/btsMMnhpu0D/fTWEiA6Xmcae0QQ7nvwE40/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eZoZye/btsMMnhpu0D/fTWEiA6Xmcae0QQ7nvwE40/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eZoZye/btsMMnhpu0D/fTWEiA6Xmcae0QQ7nvwE40/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeZoZye%2FbtsMMnhpu0D%2FfTWEiA6Xmcae0QQ7nvwE40%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;411&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 &amp;ldquo;도메인 주도 설계(Domain-Driven Design, DDD)&amp;rdquo;가 무엇인지 개념을 정리해보려고 합니다. 최근 업무를 하면서 가장 큰 고민이 좋은 설계는 어떻게 해야하는건지를 많이 생각했습니다. 사이드 프로젝트를 진행하기 전 DDD에 대해 학습하고, 이를 프로젝트에 적용하면서 실무에 도움이 될 수 있도록 하는게 이번 책을 학습하는 목표입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 도메인 주도 설계(DDD)란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어는 현실 세계의 문제를 효율적으로 해결하거나, 복잡한 비즈니스 로직을 자동화하기 위해 만들어집니다. 이때 우리가 다루는 현실 세계(또는 비즈니스 문제) 자체를 &amp;ldquo;도메인(Domain)&amp;rdquo;이라고 부릅니다. 도메인 주도 설계는 다음과 같은 특징을 갖습니다:&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;소프트웨어 각 구성 요소(Class, Interface 등)는 도메인의 핵심 개념을 &amp;ldquo;정확하게&amp;rdquo; 표현해야 한다.&lt;/li&gt;
&lt;li&gt;이를 위해서는 도메인 지식을 기반으로 한 &amp;ldquo;도메인 모델&amp;rdquo;이 설계의 중심에 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 폭포수 설계 vs 애자일, 그리고 DDD&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 설계를 하는 대표적인 방식으로 많이 알려진 &amp;lsquo;폭포수 모델(Waterfall)&amp;rsquo;과 &amp;lsquo;애자일(Agile)&amp;rsquo; 방법론이 있습니다.&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;요구사항 &amp;rarr; 분석 &amp;rarr; 설계 &amp;rarr; 개발 단계가 선형적으로 진행&lt;/b&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;도메인 주도 설계는 &amp;ldquo;설계&amp;rdquo;와 &amp;ldquo;개발&amp;rdquo;을 긴밀히 연결해, 실제 개발 과정에서 나타나는 피드백을 설계에 반영하고, 설계가 업데이트되면 개발이 더 빨라지는 선순환을 추구합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 도메인 모델(Domain Model)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델이란, 현실 도메인을 추상화하여 논리적으로 구성한 &amp;ldquo;설계의 근간&amp;rdquo;입니다. DDD에서는 이 모델이 다음과 같은 역할을 강조합니다.&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;특정 UML 다이어그램만을 모델이라 부르지 않는다. 모델은 도메인을 표현하는 모든 설계 아이디어와 개념의 뼈대이자 지향점이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 DDD의 예시로, 항공 교통 감시 시스템을 설계할 때의 간단한 모델 스케치 예시입니다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class Airplane(
    val flightNumber: String,
    val position: Coordinates,
    val scheduledRoute: List&amp;lt;Coordinates&amp;gt;
)

data class Coordinates(
    val latitude: Double,
    val longitude: Double
)

// 예: 특정 항공기가 예정된 경로를 제대로 따르는지, 충돌 위험이 없는지 등을 체크하는 기능들&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;항공 교통 관제&amp;rdquo;라는 도메인의 필수 개념(항공기, 좌표, 경로 등)을 코드 클래스에 담아내는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 유비쿼터스 언어 (Ubiquitous Language)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 주도 설계를 성공으로 이끄는 또 다른 핵심 개념은 &amp;ldquo;유비쿼터스 언어&amp;rdquo;입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 모두가 통일된 언어를 사용하자&lt;/h3&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;따라서 DDD에서는 &amp;ldquo;프로젝트 구성원 모두가 공통으로 사용하는 언어(모델 기반의 언어)&amp;rdquo;를 명시적으로 정의하라고 제안합니다. 이를 가리켜 &amp;ldquo;유비쿼터스 언어(Ubiquitous Language)&amp;rdquo;라고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 코드와 문서를 일치시키기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD에서는 모델에 해당하는 용어와 개념을 코드 레벨에서도 최대한 동일하게 유지하라고 강조합니다. 예를 들어, 항공 도메인에서 &amp;ldquo;항공기의 경로&amp;rdquo;가 &amp;ldquo;Route&amp;rdquo;라는 개념으로 정의되었다면, 코드에서도 &amp;ldquo;Route&amp;rdquo;라는 클래스로 표현하는 식입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;data class Route(
    val checkpoints: List&amp;lt;Coordinates&amp;gt;
)

fun Airplane.isFollowingRoute(currentPosition: Coordinates): Boolean {
    // 도메인에서 정의된 '경로(Route)'와 항공기의 현재 위치를 비교하는 로직
    return ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 독(주석), 스펙 문서, 기획서, 디자인 다이어그램까지 모두 같은 용어(&amp;ldquo;Route&amp;rdquo;, &amp;ldquo;Checkpoints&amp;rdquo; 등)를 사용하도록 맞춰주어야, 커뮤니케이션 오류가 줄어듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 마무리 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;도메인 주도 설계(DDD)는 소프트웨어가 해결해야 할 현실 세계의 문제(도메인)에 대한 깊은 이해를 바탕으로, &lt;b&gt;&amp;ldquo;모델&amp;rdquo;을 중심에 두고 개발과 설계를 함께 이끌어가는 방법론&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폭포수 모델, 애자일의 장&amp;middot;단점을 보완&lt;/b&gt;해, &lt;b&gt;설계와 개발이 동시에 발전하는 선순환 구조를 지향&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도메인 모델을 명확히 정의&lt;/b&gt;하고, 팀원 모두가 &lt;b&gt;이를 기반으로 소통하기 위해 &amp;ldquo;유비쿼터스 언어&amp;rdquo;&lt;/b&gt;를 사용합니다.&lt;/li&gt;
&lt;li&gt;모델의 주요 &lt;b&gt;개념과 용어를 코드에 그대로 반영&lt;/b&gt;해, &lt;b&gt;도메인 전문가와 개발자가 같은 언어로 소통&lt;/b&gt;하게 하는 것이 가장 중요합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 마지막에 있는 도메인 전문가와 개발자가 같은 언어로 소통하는게 아주아주 인상 깊었고, 와닿았습니다. 최근 개발을 진행하면서 결제 관련 로직을 수정하는데 purchase라는 구매하기 메소드를 &quot;구매&quot; 라는 용어로 내부 개발자들이 부르고 있어서 회의 중 혼란을 겪은 일이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자들간에 혼동이 생기기도 하는데 이런 부분에서 도메인 전문가(비개발자)와는 더욱 같은 언어로 소통하는게 중요한 것임을 깨달았던 것 같습니다.&lt;/p&gt;</description>
      <category>Study/Side Proejct</category>
      <category>ddd</category>
      <category>ddd란?</category>
      <category>til</category>
      <category>도메인 주도 개발</category>
      <category>애자일</category>
      <category>애자일 방법론</category>
      <category>워터폴 모델</category>
      <category>유비쿼터스 언어</category>
      <category>폭포수</category>
      <author>tomato_dev</author>
      <guid isPermaLink="true">https://mntdev.tistory.com/143</guid>
      <comments>https://mntdev.tistory.com/143#entry143comment</comments>
      <pubDate>Tue, 18 Mar 2025 00:01:45 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin Basic] 기초 문법 학습 5</title>
      <link>https://mntdev.tistory.com/142</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ey7LCj/btsMIOecHvx/KgrixYlEhv0zrML1LZ5cOK/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ey7LCj/btsMIOecHvx/KgrixYlEhv0zrML1LZ5cOK/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ey7LCj/btsMIOecHvx/KgrixYlEhv0zrML1LZ5cOK/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fey7LCj%2FbtsMIOecHvx%2FKgrixYlEhv0zrML1LZ5cOK%2Ftfile.dat&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;275&quot; height=&quot;183&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 코틀린에서 배열과 컬렉션을 어떻게 다루는지 알아보고, 이어서 확장함수(Extension Function), 중위함수(Infix Function), Inline 함수, 지역함수, 그리고 람다(Lambda)와 클로저(Closure)까지 간단히 정리해보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 배열과 컬렉션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서 배열은 상대적으로 자주 사용되지 않지만, 기본 문법을 알아두면 좋습니다. 그리고 코틀린 컬렉션은 '불변(Immutable)'인지 '가변(Mutable)'인지 미리 명시해야 하는 점이 큰 특징입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배열 (Array)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열을 선언할 때는 arrayOf() 함수를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun arraySample() {
    val array: Array&amp;lt;Int&amp;gt; = arrayOf(100, 200)

    for (i in array.indices) {
        println(&quot;${i} ${array[i]}&quot;)
    }

    for ((idx, value) in array.withIndex()) {
        println(&quot;$idx, $value&quot;)
    }

    array.plus(300)
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;array.indices : 0부터 마지막 인덱스까지의 범위를 나타냅니다.&lt;/li&gt;
&lt;li&gt;withIndex() : 인덱스와 값을 동시에 꺼낼 수 있습니다.&lt;/li&gt;
&lt;li&gt;plus() : 새로운 값을 추가한 새로운 배열을 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컬렉션 (List, Set, Map)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서 컬렉션을 만들 때는 반드시 '불변'인지, '가변'인지를 구분해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) List&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun listSample() {
    // 불변 리스트
    val numbers = listOf(100, 200)

    // 비어있는 불변 리스트 생성
    val emptyList = emptyList&amp;lt;Int&amp;gt;()

    // 값 가져오기
    println(numbers[0])

    for (number in numbers) {
        println(number)
    }

    for ((idx, value) in numbers.withIndex()) {
        println(&quot;$idx $value&quot;)
    }

    // 가변 리스트
    val mutableNumbers = mutableListOf(200, 300)
    mutableNumbers.add(400) // 가능
    for (number in mutableNumbers) {
        println(number)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;listOf() : 기본적으로 불변 리스트 생성&lt;/li&gt;
&lt;li&gt;mutableListOf() : 가변 리스트&lt;/li&gt;
&lt;li&gt;emptyList() : 비어있는 불변 리스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Set&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private fun sampleSet() {
    // 불변
    val numbers = setOf(100, 200, 200, 300)
    for (number in numbers) {
        println(number)
    }

    for ((idx, value) in numbers.withIndex()) {
        println(&quot;$idx $value&quot;)
    }

    // 가변
    val mutableSet = mutableSetOf(100, 200)
    mutableSet.add(300)
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복을 허용하지 않는 구조로, 빠른 검색에 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) Map&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun mapSample() {
    // 가변
    val oldMap = mutableMapOf&amp;lt;Int, String&amp;gt;()
    oldMap[1] = &quot;MON&quot;
    oldMap[2] = &quot;TUES&quot;

    // 불변
    val mapOf: Map&amp;lt;Int, String&amp;gt; = mapOf(1 to &quot;MON&quot;, 2 to &quot;TUES&quot;)

    for (key in oldMap.keys) {
        println(&quot;$key : ${oldMap[key]}&quot;)
    }

    for ((key, value) in oldMap.entries) {
        println(&quot;$key $value&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mapOf(key to value) : 불변 map 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컬렉션의 Null 가능성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;List&amp;lt;Int?&amp;gt; : 리스트에는 null이 들어갈 수 있으나, 리스트 자체는 null이 아님&lt;/li&gt;
&lt;li&gt;List? : 리스트 자체가 null 일 수 있지만, 요소들에는 null이 들어갈 수 없음&lt;/li&gt;
&lt;li&gt;List&amp;lt;Int?&amp;gt;? : 리스트도 null일 수 있고, 요소에도 null이 들어갈 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 확장함수와 확장 프로퍼티&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin에는 '확장함수'가 있어, 클래스 밖에서 함수나 프로퍼티를 추가하되 마치 클래스 내부의 멤버처럼 호출할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확장함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun String.customLastChar(): Char {
    return this[this.length - 1]
}

fun main() {
    val str = &quot;ABC&quot;
    println(str.customLastChar()) // 'C'
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 String 클래스 내부에 존재하지 않는 함수를 확장함수로 추가하여 사용&lt;/li&gt;
&lt;li&gt;클래스의 private, protected 멤버에는 접근할 수 없음 (캡슐화는 여전히 보호)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확장 프로퍼티&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;val String.lastChar: Char
    get() = this[this.length - 1]

val Person.customLastNamePlusMj: String
    get() = this.lastName + &quot;Mj&quot;

fun main() {
    val str: String = &quot;ABC&quot;
    println(str.lastChar) // 'C'

    val person = Person(&quot;MJ&quot;, &quot;Kim&quot;, 20)
    println(person.customLastNamePlusMj) // 'KimMj'
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멤버 변수를 직접 추가한 것처럼 보이지만, 실제로는 getter를 확장해 만들어낸 방식&lt;/li&gt;
&lt;li&gt;확장함수와 동일하게 private 멤버에는 접근 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 중위함수 (Infix Function)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;함수를 호출하는 새로운 방법!&amp;rdquo;이라는 말처럼, infix 키워드를 붙이면 함수를 연산자처럼 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun Int.add(other: Int): Int {
    return this + other
}

infix fun Int.add2(other: Int): Int {
    return this + other
}

fun main() {
    println(3 add2 4) // infix call
    println(3.add2(5)) // 일반 호출
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;infix로 선언한 함수는 &amp;ldquo;수식처럼&amp;rdquo; 가독성 좋게 쓸 수 있음&lt;/li&gt;
&lt;li&gt;인자가 하나만 필요하거나, 둘 이상의 인자를 받으면 파라미터를 묶어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Inline 함수와 지역 함수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Inline 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 호출하는 대신, 함수를 호출한 지점에 함수 본문을 그대로 복사해 붙이는 개념입니다. 함수를 파라미터로 전달할 때 발생하는 오버헤드를 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;inline fun Int.addCustom(other: Int): Int {
    return this + other
}&lt;/code&gt;&lt;/pre&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;h3 data-ke-size=&quot;size23&quot;&gt;지역함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 안에 함수를 선언하는 기능입니다. 하지만 별도의 함수로 분리하는 게 가독성 면에서 더 낫다는 의견이 많아 상대적으로 잘 쓰이지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun createPersonUsingLocalFunction(
    firstName: String,
    lastName: String
): Person {

    fun validateName(name: String, fieldName: String) {
        if(name.isEmpty()) {
            throw IllegalArgumentException(&quot;$fieldName is empty.&quot;)
        }
    }

    validateName(firstName, &quot;first name&quot;)
    validateName(lastName, &quot;last name&quot;)

    return Person(firstName, lastName, 1)
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 깊이가 깊어지고, 여러 책임을 한 함수에 몰아넣게 될 수 있어 자주 권장되진 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 람다(Lambda)와 클로저(Closure)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 함수가 일급 시민(First-class citizen)이므로, 변수에 할당하거나, 파라미터로 넘기는 것이 자유롭습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;람다 문법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;val isApple = { fruit: Fruit -&amp;gt; fruit.name == &quot;사과&quot; }

fun main() {
    val fruits = listOf(
        Fruit(&quot;사과&quot;, 1000),
        Fruit(&quot;바나나&quot;, 3000)
    )

    println(isApple(fruits[0]))    // true
    println(isApple.invoke(Fruit(&quot;메론&quot;, 2000))) // false
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(Fruit) -&amp;gt; Boolean : 람다의 타입&lt;/li&gt;
&lt;li&gt;invoke() 로 명시적으로 호출 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클로저(Closure)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다가 시작되는 시점에, 참조하고 있는 외부 변수들을 함께 포획(Capture)해서 유지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java (개념 비교)&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String targetFruitName = &quot;바나나&quot;;
targetFruitName = &quot;수박&quot;; 
// Variable used in lambda expression should be final or effectively final
filterFruits(fruits, fruit -&amp;gt; targetFruitName.equals(fruit.getName())); 
// 컴파일 에러&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;var targetFruitName = &quot;바나나&quot;
targetFruitName = &quot;수박&quot;
println(filterFruits(fruits) { it.name == targetFruitName })&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코틀린에선 람다가 사용하는 변수가 final이 아니어도 문제없이 동작&lt;/li&gt;
&lt;li&gt;람다 내부에서 필요한 외부 변수들을 람다 실행 시점에 모두 포획해 두는 구조&lt;br /&gt;&amp;rarr; 이를 클로저라고 함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 마무리 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;배열(arrayOf)과 다양한 컬렉션(List, Set, Map)에서 불변/가변을 명시 &amp;rarr; 의도된 코드 작성&lt;/li&gt;
&lt;li&gt;확장함수와 확장 프로퍼티로 클래스 외부에서 멤버를 추가하듯 확장&lt;/li&gt;
&lt;li&gt;중위함수(infix)로 연산자처럼 가독성 있게 함수 호출&lt;/li&gt;
&lt;li&gt;inline 함수는 오버헤드를 줄이지만, 성능 측정이 필수&lt;/li&gt;
&lt;li&gt;지역함수는 가독성을 해칠 수 있어 사용 시 주의&lt;/li&gt;
&lt;li&gt;코틀린 람다는 함수가 일급 시민이므로 자유로운 활용 가능 &amp;rarr; 클로저로 외부 변수까지 포획&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Languege/Kotlin</category>
      <category>kotlin array</category>
      <category>kotlin infix</category>
      <category>kotlin inline</category>
      <category>Kotlin 배열</category>
      <category>지역함수</category>
      <category>코틀린</category>
      <category>코틀린 람다</category>
      <category>코틀린 중위함수</category>
      <category>코틀린 컬렉션</category>
      <category>코틀린 클로저</category>
      <author>tomato_dev</author>
      <guid isPermaLink="true">https://mntdev.tistory.com/142</guid>
      <comments>https://mntdev.tistory.com/142#entry142comment</comments>
      <pubDate>Wed, 12 Mar 2025 23:51:48 +0900</pubDate>
    </item>
  </channel>
</rss>