<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>The Nirsa Way</title>
    <link>https://nirsa.tistory.com/</link>
    <description>Email: islandtim@naver.com
Blog: https://nirsa.tistory.com
GitHub: https://github.com/KoreaNirsa</description>
    <language>ko</language>
    <pubDate>Tue, 25 Aug 2026 12:22:26 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>KoreaNirsa</managingEditor>
    <image>
      <title>The Nirsa Way</title>
      <url>https://tistory1.daumcdn.net/tistory/3406000/attach/d7862faaa7ce4723b4a6f06ae5472b12</url>
      <link>https://nirsa.tistory.com</link>
    </image>
    <item>
      <title>최근 AX 직무 면접을 보며 느낀 이야기 - 개발자가 없는 개발팀</title>
      <link>https://nirsa.tistory.com/496</link>
      <description>&lt;blockquote data-end=&quot;220&quot; data-start=&quot;163&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최근 AX 직무 면접을 보며 느낀 이야기 - 개발자가 없는 개발팀&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 AX(AI Transformation) 관련 포지션의 면접을 여러 차례 경험했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기업마다 AX를 정의하는 방식은 조금씩 달랐지만, 면접을 거듭할수록 비슷한 이야기를 반복해서 듣게 되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI를 적극적으로 활용합니다.&lt;/li&gt;
&lt;li&gt;최대한 빠르게 제품을 만듭니다.&lt;/li&gt;
&lt;li&gt;한 사람이 하나의 제품을 책임집니다.&lt;/li&gt;
&lt;li&gt;가능하면 한 달 안에 결과물이 나와야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 어떤 조직에서는 코드 품질이나 협업 방식은 현재 중요한 문제가 아니라고 이야기하기도 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 개발 방식의 차이라고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 비슷한 이야기를 계속 듣다 보니 한 가지 의문이 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;AI를 이용해 제품을 빠르게 만드는 것과, 그 제품을 몇년동안 유지하고 운영하는 것은 같은 문제일까요?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 대답은 그렇지 않다는 쪽에 가깝습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개발자가 없는 AX팀의 AI-Native 개발자&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 면접에서는 다음과 같은 대화를 나눴습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;면접관&lt;/b&gt;: &quot;우리 AX팀은 AI-Native 개발자들입니다.&quot;&lt;br /&gt;&lt;b&gt;나&lt;/b&gt;: &quot;개발자 출신들로 이루어진 팀인가요?&quot;&lt;br /&gt;&lt;b&gt;면접관&lt;/b&gt;: &quot;비개발자들로 이루어져 있습니다.&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이야기를 듣고 가장 먼저 떠올랐던 질문은 이것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발자가 없는 개발팀에서 제품의 기술적인 품질은 누가 판단할까요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 비개발자가 AI를 이용해 프로그램을 만드는 것 자체를 부정적으로 생각하지는 않으며 오히려 AI가 가져온 가장 큰 변화 중 하나라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획자가 자신의 반복 업무를 자동화할 수 있고, HR 담당자가 간단한 사내 도구를 만들 수도 있습니다. 또는 마케터가 데이터를 분석하거나 업무용 애플리케이션을 직접 만들 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거라면 개발자에게 요청해야 했던 많은 작업을 현업 담당자가 직접 해결할 수 있게 되었으며 이것은 분명한 발전입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 저는 &lt;b&gt;어디까지 AI에게 맡길 것인가&lt;/b&gt;는 구분해야 한다고 생각합니다. 문제는 PoC를 만드는 방식과 실제 제품을 만드는 방식을 동일하게 바라보기 시작할 때 발생합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;코드 품질은 크게 생각하지 않습니다&quot;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 면접에서는 다음과 비슷한 이야기를 들었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;면접관&lt;/b&gt; : &quot;우리는 코드 품질에 대해서는 크게 생각하고 있지 않습니다.&quot;&lt;br /&gt;&lt;b&gt;면접관&amp;nbsp;&lt;/b&gt;:&amp;nbsp;&quot;1인 1제품을 담당합니다.&quot;&lt;br /&gt;&lt;b&gt;면접관&amp;nbsp;&lt;/b&gt;: &quot;각각의 솔루션이 한 달 단위로 MVP가 나와야 합니다.&quot;&lt;br /&gt;&lt;b&gt;면접관&amp;nbsp;&lt;/b&gt;:&amp;nbsp;&quot;사실상 협업은 하지 않습니다.&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 실험이 목적이라면 이러한 방식에도 분명 장점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의사결정이 빠릅니다.&lt;/li&gt;
&lt;li&gt;회의가 줄어듭니다.&lt;/li&gt;
&lt;li&gt;코드 리뷰를 기다릴 필요도 없습니다.&lt;/li&gt;
&lt;li&gt;한 사람이 제품 하나를 빠르게 완성할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 PoC가 Product가 되었을 때 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5910&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A4Aai/dJMcaaGxPuB/T4irz5gPPNU1oVnT0yVEn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A4Aai/dJMcaaGxPuB/T4irz5gPPNU1oVnT0yVEn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A4Aai/dJMcaaGxPuB/T4irz5gPPNU1oVnT0yVEn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA4Aai%2FdJMcaaGxPuB%2FT4irz5gPPNU1oVnT0yVEn1%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;5910&quot; height=&quot;305&quot; data-origin-width=&quot;5910&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &quot;일단 빠르게 만들고, 잘되면 그때 정리하면 되지 않을까?&quot; 하고 생각할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 사용자가 생기고 업무가 그 시스템에 의존하기 시작하면 이야기가 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때부터 기존 코드를 다시 만드는 것은 더 어려워지며 결국 빠르게 만들어 놓았던 코드 위에 새로운 기능이 계속 추가되기 시작합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 AI가 정말 잘합니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI-Native 개발의 위험성을 이야기하기 전에 한 가지는 분명히 하고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;초기 개발에서는 AI가 정말 잘합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면을 만들어 달라고 하면 만들어 주고, API를 작성해 달라고 하면 작성해줍니다. 또한 데이터베이스를 연결해 달라고 하면 연결하고, 기존 로직을 변경해 달라고 하면 관련 코드를 찾아 상당히 그럴듯하게 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 초기 몇 달 동안은 굉장한 생산성 향상이 나타날 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에 일주일 걸렸던 작업이 하루 만에 끝나며, 한 명이 여러 명의 개발자가 하던 일을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 조직 입장에서는 &quot;개발자가 없어도 되는 것 아닌가?&quot; 하고 생각하기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 우려하는 것은 바로 그다음부터입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;문제는 제품이 계속 변경된다는 것입니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 A라는 파일이 있다고 가정해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A에는 약 1,000줄의 코드가 들어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 정상적으로 사용하던 핵심 로직입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 지나 요구사항이 변경됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 AI에게 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;OO 기능들이 사용하는 A에 있는 이 로직을 새로운 방식으로 변경해줘.&quot;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 코드를 분석합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 새로운 B 파일을 만들어 변경된 로직을 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관된 코드 일부도 B를 바라보도록 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 문제가 없어 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 A 파일이 완전히 사라진 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 코드는 여전히 다른 곳에서 사용하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 코드는 B로 대체되면서 더 이상 사용되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 일부 코드는 사용 중인지 아닌지 바로 판단하기 어려울 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 몇 달 뒤 요구사항이 변경됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI에게 다시 수정해 달라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 C가 만들어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 변경되면 D가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 변경되면 E가 생길 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4224&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVy1TY/dJMcaasYhu4/kDUxGWlJU9GjQyx2O35Z2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVy1TY/dJMcaasYhu4/kDUxGWlJU9GjQyx2O35Z2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVy1TY/dJMcaasYhu4/kDUxGWlJU9GjQyx2O35Z2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVy1TY%2FdJMcaasYhu4%2FkDUxGWlJU9GjQyx2O35Z2K%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;4224&quot; height=&quot;1080&quot; data-origin-width=&quot;4224&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 코드는 계속 생깁니다. 또한 과거의 코드가 항상 완벽하게 사라지지 않을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 AI가 만든 코드에 대해서 리뷰를 한다면 이러한 문제점은 커지기 전에 찾을 수 있겠지만, 비개발자는 코드를 읽을줄 모르기에 이상한점을 알 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI로 리뷰를 하더라도 A의 남아있는 로직은 일부 기능에서 필요하다고 판단되기에 이 코드 또한 문제 없다고 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터 문제가 시작됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1,000줄 중 300줄만 사용한다면 A는 사용 중인 파일일까요?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 파일이 1,000줄이라고 가정하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중 700줄은 더 이상 사용되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;300줄은 실제 시스템에서 호출되고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 A는 사용 중인 파일일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적으로는 그렇습니다. 300줄이 사용되고 있기 때문에 파일 전체를 삭제하면 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 상황을 조금 바꿔보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 300줄 역시 사실은 이미 B의 새로운 비즈니스 로직으로 변경되어야 했던 과거 코드라고 가정해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 코드가 아직 A를 바라보고 있기 때문에 실행되고 있을 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이야기가 달라집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3624&quot; data-origin-height=&quot;885&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w1WX1/dJMcaiqUZ5c/SJkDk7QnZIy1L1dK3hOXiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w1WX1/dJMcaiqUZ5c/SJkDk7QnZIy1L1dK3hOXiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w1WX1/dJMcaiqUZ5c/SJkDk7QnZIy1L1dK3hOXiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw1WX1%2FdJMcaiqUZ5c%2FSJkDk7QnZIy1L1dK3hOXiK%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;3624&quot; height=&quot;885&quot; data-origin-width=&quot;3624&quot; data-origin-height=&quot;885&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황에서 중요한 차이가 하나 생기는데, 참조되고 있는 코드와 필요한 코드는 같은 의미가 아니라는 것 입니다.&lt;b&gt; &lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;코드가 실행된다고 해서 올바른 코드라는 보장은 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 코드에서 직접 사용 흔적이 잘 보이지 않는다고 해서 반드시 삭제해도 된다는 의미도 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 시스템에서는 설정이나 프레임워크, 외부 시스템과의 연결 때문에 예상하지 못한 방식으로 코드가 사용될 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 중요한 것은 단순히 &quot;이 코드가 호출되고 있는가?&quot;가 아니라, &quot;이 코드가 현재 비즈니스 규칙에서 여전히 존재해야 하는가&quot;를 판단하는 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코드를 만드는 것은 쉬워졌지만, 무엇을 지워도 되는지는 쉬워지지 않았습니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이 지점이 AI-Native 개발에서 굉장히 중요하다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI에게 새로운 기능을 만들어 달라고 하는 것은 점점 쉬워지고 있으며, 반대로 이미 쌓여 있는 코드 중 무엇을 없애도 되는지 판단하는 것은 여전히 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 시스템이 커질수록 더 어려워지며&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 어느 순간 &quot;사용하지 않는 코드와 중복된 파일을 전부 정리해줘.&quot;와 같은 요청을 하게 될 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 기능 하나가 잘못되었다면 해당 기능을 다시 수정하면 되지만, 기존 코드를 잘못 삭제하면 전혀 예상하지 못했던 기존 기능이 깨질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 큰 문제는 테스트와 명세가 충분하지 않은 경우입니다. A, B, C, D가 모두 존재하고 각각 일부 코드에서 사용되고 있다면 AI는 결국 현재 존재하는 코드와 주변 정보를 보고 판단해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 코드 자체가 이미 여러 세대의 비즈니스 로직이 뒤섞인 상태라면 어떤 것이 진짜 기준일까요?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AI에게도 결국 진실의 기준(&lt;b&gt;Source of Truth&lt;/b&gt;)이 필요합니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;2962&quot; data-origin-height=&quot;1570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LKiuE/dJMcadpAMXQ/vKQewOfbksu2dUEnoBklxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LKiuE/dJMcadpAMXQ/vKQewOfbksu2dUEnoBklxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LKiuE/dJMcadpAMXQ/vKQewOfbksu2dUEnoBklxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLKiuE%2FdJMcadpAMXQ%2FvKQewOfbksu2dUEnoBklxK%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;572&quot; height=&quot;303&quot; data-origin-width=&quot;2962&quot; data-origin-height=&quot;1570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A, B, C는 모두 실행되며 컴파일도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 AI에게 &quot;잘못된 코드를 정리해줘.&quot; &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;라고 요청했을 때 무엇을 기준으로 판단해야 할까요? &lt;/span&gt;결국 기준이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 요구사항이 무엇인지, 어떤 비즈니스 규칙이 맞는지, 어떤 동작을 반드시 유지해야 하는지, 어떤 기능은 이미 폐기되었는지를 알아야 합니다. 즉 코드만으로는 부족합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명세, 테스트, 문서, 변경 이력, 운영 정책과 같은 기준이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 누군가는 AI가 내린 판단이 정말 맞는지도 검증해야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 &quot;사용하지 않는 코드를 정리해 줘&quot;도 간단하지 않습니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 코드가 너무 복잡해졌다고 생각해 AI에게 다음과 같이 요청했다고 가정해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;사용하지 않는 코드와 중복된 파일을 전부 정리해줘.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 현재 코드의 참조 관계와 주변 맥락을 분석합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 B.java가 최신 정책을 구현한 코드라고 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A.java에는 과거 정책이 남아 있고, 상당 부분이 B로 대체되었다고 판단하여 A를 제거합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업도 정상적으로 완료된 것처럼 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 배포한 뒤 기존에 정상적으로 동작하던 OO 기능이 동작하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고 보니 해당 기능의 일부가 여전히 A의 코드를 사용하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 다시 AI에게 이야기합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;아니, 사용하지 않는 코드를 정리하라고 했는데 이번 작업 이후 OO 기능이 안되잖아.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 변경 이력을 확인하고 A를 다시 복구합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능은 다시 정상적으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 사용자의 원래 목적은 여전히 해결되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A에는 사용하지 않는 코드가 많이 남아 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터 문제가 반복될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사례에서 중요한 것은 AI가 무능해서 잘못 판단했다는 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 입장에서는 상당히 합리적인 판단을 했을 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A에는 과거 정책이 존재하며, B에는 최신 정책이 존재합니다. 그렇다면 A는 제거 대상처럼 보일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 시스템에서는 일부 기능이 여전히 A를 호출하고 있었습니다. 그렇다고 A를 그대로 유지하는 것도 정답은 아니게 됩니다. A 안에는 정말로 필요 없는 코드가 많이 남아 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 문제는&lt;b&gt; &quot;A를 삭제할것인가, 유지할것인가?&quot;&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;가 아니라, &quot;&lt;b&gt;A의 어느 부분은 현재도 반드시 필요하고, 어느 부분은 이미 폐기되어야 하며, 현재 사용 중인 코드 중에서도 잘못된 과거 정책을 사용하는 부분은 어디인가?&quot;&lt;/b&gt;를 판단해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 훨씬 어려운 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 시스템에 명확한 &lt;b&gt;Source of Truth&lt;/b&gt;가 없다면 AI는 현재 존재하는 코드와 호출 관계를 다시 근거로 판단할 수밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 바로 그 코드와 호출 관계가 이미 여러 차례의 AI 수정으로 뒤섞여 있는 상태라면 판단의 기준 자체가 흔들립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음과 같은 상황이 만들어질 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;A.java 삭제
    &amp;darr;
기존 기능 장애
    &amp;darr;
A.java 복구
    &amp;darr;
불필요한 코드는 다시 살아남음
    &amp;darr;
다시 정리 요청
    &amp;darr;
또 다른 영향 범위 분석
    &amp;darr;
삭제와 복구 반복
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 AI를 사용한 이유는 사람의 개발 비용을 줄이기 위해서였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이러한 상태까지 코드가 누적되면 새로운 코드를 만드는 비용보다 &lt;b&gt;무엇을 지워도 되는지 확인하는 비용&lt;/b&gt;이 더 커질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 필요한 것은 현재 비즈니스 규칙을 명확하게 정의한 기준과 테스트, 그리고 변경 결과가 올바른지 판단할 수 있는 엔지니어링 역량입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AI Slop은 코드베이스에서도 생길 수 있습니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이런 현상을 코드의 AI Slop이라고 볼 수도 있다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Slop이라고 해서 반드시 코드 한 줄 한 줄이 형편없다는 의미는 아닙니다. 오히려 각각을 보면 정상적으로 동작할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 그것들이 계속 누적된 결과입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;4685&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnOkZL/dJMcad4bBbo/0HwUa63GK2ZctYfIko0D6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnOkZL/dJMcad4bBbo/0HwUa63GK2ZctYfIko0D6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnOkZL/dJMcad4bBbo/0HwUa63GK2ZctYfIko0D6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnOkZL%2FdJMcad4bBbo%2F0HwUa63GK2ZctYfIko0D6k%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;282&quot; height=&quot;940&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;4685&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업이 수십 번, 수백 번 반복되면 코드베이스 안에는 여러 시점의 진실이 동시에 존재하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 새로운 기능을 빠르게 만드는 것이 생산성처럼 보였으나 시간이 흐르면서 이제 새로운 기능 하나를 변경하기 위해 기존에 어떤 코드가 살아 있는지부터 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 과거에 아낀 시간이 미래의 검증 비용으로 돌아갑니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개발자들은 이미 오래전부터 이 문제와 싸워왔습니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이런 문제는 AI가 등장하면서 처음 생긴 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dead Code, Legacy Code, Spaghetti Code,&amp;nbsp;Technical Debt, Regression, Knowledge Silo 같은 문제는 소프트웨어 업계가 수십 년 동안 경험해 온 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 개발 조직은 여러 가지 안전장치를 만들어 왔습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;위험&lt;/td&gt;
&lt;td&gt;일반적으로 사용하는 방법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;잘못된 코드 변경&lt;/td&gt;
&lt;td&gt;Code Review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기존 기능 파괴&lt;/td&gt;
&lt;td&gt;Automated Test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;큰 변경의 위험&lt;/td&gt;
&lt;td&gt;작은 단위의 점진적인 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;변경 이력 확인&lt;/td&gt;
&lt;td&gt;Git / Version Control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 품질 문제&lt;/td&gt;
&lt;td&gt;Static Analysis / Quality Gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포 오류&lt;/td&gt;
&lt;td&gt;CI/CD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 중 문제 확인&lt;/td&gt;
&lt;td&gt;Logging / Monitoring / Tracing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설계 의도 소실&lt;/td&gt;
&lt;td&gt;문서 / ADR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특정 개인에게만 지식 집중&lt;/td&gt;
&lt;td&gt;Review / 협업 / Shared Ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 AI 개발이 들어서며 바이브 코딩, 컨텍스트 엔지니어링, 스펙주도개발, 하네스엔지니어링, 루프엔지니어링과 같은 방법론들이 계속해서 나타나고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방법들이 존재하는 이유는 개발자가 보수적이어서가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소프트웨어가 커질수록 개인의 기억과 판단만으로 시스템을 안전하게 유지하기 어렵다는 경험이 축적되었기 때문입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 개발자가 완벽하다는 이야기가 아닙니다. 오히려 개발자도 실수하기 때문에 여러 사람이 검토하고, 테스트하고, 자동화하고, 기록하는 구조를 만들어 온 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그런데 일부 AX 조직에서는 그 안전장치를 병목으로 바라봅니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 최근 몇몇 AX 면접에서 가장 우려했던 부분도 여기에 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리뷰를 하지 않습니다.&lt;/li&gt;
&lt;li&gt;협업하지 않습니다.&lt;/li&gt;
&lt;li&gt;각자 하나의 제품을 담당합니다.&lt;/li&gt;
&lt;li&gt;코드 품질보다는 빠른 속도로 제품이 나오는 것이 중요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 것은 단기적인 성과라는 관점에서는 굉장히 효율적으로 보일 수 있습니다. 특히 요즘처럼 AX와 혁신이라는 키워드로 진행되는 사내 AX 프로젝트들은 더더욱 그렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 생각해 볼 필요가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리뷰를 제거하면 리뷰에 걸리는 시간은 사라집니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 잘못된 코드가 들어갈 가능성도 함께 사라질까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트를 줄이면 테스트 작성 시간은 줄어듭니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 기존 기능이 깨질 가능성도 함께 줄어들까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문서를 만들지 않으면 문서 작성 시간은 사라집니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 담당자가 퇴사했을 때 시스템을 이해하는 비용도 사라질까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 병목을 제거한 것이 아니라 미래로 미루는 겁니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;빠른 개발의 비용은 당장 보이지 않습니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 기술 부채가 위험한 이유입니다. 기술 부채는 코드를 작성한 당일에는 거의 보이지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스는 정상적으로 실행됩니다.&lt;/li&gt;
&lt;li&gt;화면도 잘 나옵니다.&lt;/li&gt;
&lt;li&gt;사용자도 기능을 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 성공한 것처럼 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 요구사항이 계속 변경되면서 비용이 나타나기 시작합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;7048&quot; data-origin-height=&quot;1380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cx7GwY/dJMcaf8JWfY/kKT2p4fRKVrP1O9WwwAOs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cx7GwY/dJMcaf8JWfY/kKT2p4fRKVrP1O9WwwAOs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cx7GwY/dJMcaf8JWfY/kKT2p4fRKVrP1O9WwwAOs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcx7GwY%2FdJMcaf8JWfY%2FkKT2p4fRKVrP1O9WwwAOs0%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;7048&quot; height=&quot;1380&quot; data-origin-width=&quot;7048&quot; data-origin-height=&quot;1380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이러니하게도 처음에는 가장 빠르게 개발했던 조직이 몇 년 뒤에는 하나의 기능을 변경하는 데 가장 많은 시간이 필요한 조직이 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개발자가 없는 AX 개발팀의 한계는 '코딩 능력'이 아닙니다.&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 제가 이야기하고 싶은 핵심이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비개발자가 코드를 직접 작성할 수 없어서 문제라고 생각하는 것이 아닙니다. 이제&amp;nbsp;상당 부분은 AI가 작성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 더 중요하게 생각하는 차이는 &lt;b&gt;무엇을 의심해야 하는지를 아는가&lt;/b&gt;입니다. 이러한 것은 기본적으로 개발과 여러 CS 지식들이 합쳐지며 의문이 나타날 수 있으며, 이는 비개발자의 한계이기도 합니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 기능이 어떤 상황에서 실패할 것인가?&lt;/li&gt;
&lt;li&gt;기존 기능에 영향을 받는가?&lt;/li&gt;
&lt;li&gt;잘못된 데이터가 들어오면 어떻게 처리하는가?&lt;/li&gt;
&lt;li&gt;동시에 여러 요청이 오면 어떻게 되는가?&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;이런 질문들은 프로그래밍 언어의 문법을 많이 안다는 의미와는 조금 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 번의 변경, 장애, 운영, 유지보수를 경험하고 지속적으로 CS에 대한 공부를 하면서 생기는 사고방식에 더 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 개발자가 없는 개발팀의 가장 큰 위험이 &lt;b&gt;만들어진 코드가 언제 위험해지는지 알아차리지 못하는 것&lt;/b&gt;이라고 생각합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AX 조직 자체가 문제라는 의미는 아닙니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 경험했던 몇몇 AX 조직은 기존 개발 조직과 별도로 구성되어 있었고, C-Level 직속 조직으로 빠르게 만들어진 경우들도 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C-Level 직속 조직 자체가 잘못되었다고 생각하지는 않습니다. 오히려 AX처럼 전사적인 변화가 필요한 조직에서는 강한 의사결정 권한이 도움이 될 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 우려하는 것은 다른 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조직 안에서 기술적 의사결정을 견제할 사람이 없는 경우입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리더도 개발 경험이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원도 개발 경험이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리더가 개발 경험이 있더라도 팀원들이 개발을 모르기에 기술적 리딩은 불가능 합니다. 개발에 대한 커뮤니케이션도 불가능합니다. 개발팀이지만 개발을 모르기에 기술적 용어는 되도록 사용하지 않아야 하며 쉽게 설명해주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰하는 사람도 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 개발 조직과 교류도 거의 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면서 실제 운영 제품을 계속 만들어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조에서는 &quot;이렇게 하면 특정 상황에서 문제가 생길 수 있습니다.&quot; &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;라고 이야기할 사람이 없어집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당장 제품이 동작한다면 이러한 기술적인 의문은 쉽게 후순위로 밀리게 됩니다. 당연히 이러한 상황에서의 문제는 몇 년뒤 서서히 찾아오게 됩니다. 하지만 그 때쯤이면 이미 수없이 쌓인 AI Slop에 의해 오히려 새로 개발하는게 더 빠를 것 입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AX의 KPI가 '몇 개 만들었는가'만 되어서는 안 됩니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AX 조직은 빠르게 성과를 증명해야 한다는 압박을 받기 쉽습니다. 그래서 다음과 같은 지표가 중요해질 수 있습니다.&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;직원들이 AI를 얼마나 많이 사용했는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 필요한 지표입니다. 위와 같은 지표만을 본다면 AI가 만든 결과물이 엄청난 것으로 보이고, 성과도 잘 나오며, 단기적으로 봣을 때 당장 엄청난 아웃풋이 나오기에 성공적인 것처럼 보일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 저는 장기적인 AX라면 다른 질문도 함께 해야 한다고 생각합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 만들어진 시스템 중 6개월 뒤에도 실제로 사용되는 것은 몇 개인가? &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 요구사항을 변경하는 데 얼마나 많은 시간이 필요한가? &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;담당자가 바뀌어도 다른 사람이 운영할 수 있는가?&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;장애가 발생했을 때 원인을 찾고 복구할 수 있는가?&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;AI가 생성한 코드가 어떤 기준으로 검증되고 있는가?&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AX의 성과를 아웃풋으로만 평가하면 새로운 제품은 계속 만들어지고 단기적으로는 좋은 성과를 낼 수 있습니다. 하지만 조직 안의 기술 부채 역시 그보다 빠르게 증가하게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;일부 AX 조직을 비관적으로 바라보는 이유&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 AX 면접들을 경험하면서 제가 가장 우려했던 것은 AI 기술의 수준이 아니었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 AI를 바라보는 방식이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI를 도입하면 개발 과정의 많은 부분을 단축할 수 있습니다. 이것은&amp;nbsp;사실입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 일부 조직에서는 여기서 한 단계 더 나아가 &quot;AI가 있으니 기존 개발 방식은 필요하지 않다.&quot; &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;는 방향으로 가고 있다는 인상을 받았습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이것이 위험하다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 코드를 만드는 비용을 크게 낮췄지만 문제는 코드 생성 비용이 낮아졌다고 해서 유지보수 비용까지 자동으로 낮아지는 것은 아니라는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 검증 구조 없이 코드 생성량만 폭발적으로 증가한다면 기술 부채 역시 과거보다 훨씬 빠르게 증가할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; AI-Native는 Developer-less가 아닙니다.&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;제가 AI-Native 자체를 부정적으로 바라보는 것은 아닙니다. 오히려 앞으로의 개발은 점점 더 AI-Native에 가까워질 것이라고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AI가 코드를 작성하고, 테스트를 만들며, 코드 리뷰를 돕습니다. 또한 장애 로그를 분석하고 문서를 작성하며, AI AGent가 여러 작업을 나누어 수행할수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발자의 역할 역시 코드를 직접 입력하는 비중에서 점점 더 문제를 정의하고, 구조를 설계하고, AI에게 일을 나누고, 결과를 검증하고, 최종적인 품질을 책임지는 방향으로 이동하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;제가 생각하는 AI-Native는 AI를 이용해 소프트웨어 엔지니어링을 없애는 것이 아니라, 소프트웨어 엔지니어링을 더 빠르고 강하게 만드는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;AI-Native는 Developer-less가 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마치며&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음의 질문으로 돌아가 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비개발자들만으로 이루어진 AI-Native 개발팀이 장기간 제품을 만들고 운영할 수 있을까요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 현재 대답은 부정적입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개인 업무 자동화는 가능합니다.&lt;/li&gt;
&lt;li&gt;PoC도 가능합니다.&lt;/li&gt;
&lt;li&gt;프로토타입도 가능합니다.&lt;/li&gt;
&lt;li&gt;간단한 내부 도구도 충분히 만들 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그 외의 목적으로 전사적으로 도입하는 솔루션이 되거나, 외부로 나가야 하는 솔루션이라면 분명히 한계를 느끼는 지점은 다가올것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제품이 커지고, 중요한 비즈니스 로직이 들어가고, 수년 동안 요구사항이 변경되고, 여러 시스템과 연결되고, 실제 사용자의 업무가 그 시스템에 의존하기 시작하면 다른 문제가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 순간부터 중요한 것은 &quot;AI가 이 코드를 만들 수 있는가?&quot;&lt;b&gt; &lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;가 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&quot;현재 존재하는 코드 중 무엇이 맞고, 무엇이 잘못되었으며, 무엇을 없애도 되는지를 누가 판단할 수 있는가?&quot;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI에게 A의 로직을 바꾸라고 해서 B를 만드는 것은 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B를 다시 C로 변경하는 것도 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D와 E를 만드는 것도 어렵지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 수년 뒤 A, B, C, D, E가 뒤섞인 코드베이스에서 어떤 코드가 현재의 진실이고, 어떤 코드가 과거의 흔적이며, 어떤 코드를 삭제해도 되는지를 판단하는 것은 전혀 다른 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 AI 시대에 소프트웨어 엔지니어링이 덜 중요해지는 것이 아니라 오히려 더 중요해질 수 있다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 만드는 것은 쉬워졌지만, 무엇을 지워도 되는지 판단하는 것은 쉬워지지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조되고 있는 코드와 필요한 코드는 같은 의미가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 결국, AI-Native는 Developer-less가 아니라 소프트웨어 엔지니어링을 AI로 가속하는 방향이어야 한다고 생각합니다.&lt;/p&gt;</description>
      <category>AI Engineering/Methodology</category>
      <author>KoreaNirsa</author>
      <guid isPermaLink="true">https://nirsa.tistory.com/496</guid>
      <comments>https://nirsa.tistory.com/496#entry496comment</comments>
      <pubDate>Sat, 22 Aug 2026 23:03:30 +0900</pubDate>
    </item>
    <item>
      <title>Codex App + Hermes Desktop으로 AI 개발 환경 구축하기 (1) - Hermes란? Codex와 함께 사용하는 이유</title>
      <link>https://nirsa.tistory.com/495</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Codex&amp;nbsp;App&amp;nbsp;+&amp;nbsp;Hermes&amp;nbsp;Desktop으로&amp;nbsp;AI&amp;nbsp;개발&amp;nbsp;환경&amp;nbsp;구축하기&amp;nbsp;(1)&amp;nbsp;-&amp;nbsp;Hermes란?&amp;nbsp;Codex와&amp;nbsp;함께&amp;nbsp;사용하는&amp;nbsp;이유&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 AI를 활용한 개발 방식은 빠르게 변화하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 ChatGPT에 코드를 붙여 넣고 질문하거나, IDE의 AI 플러그인을 이용해 코드를 생성하는 것이 일반적이었습니다. 하지만 최근에는 AI가 프로젝트를 직접 이해하고, 코드를 수정하며, 테스트와 Git 작업까지 수행하는 &lt;b&gt;AI Agent&lt;/b&gt; 형태의 개발 방식이 등장하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예가 &lt;b&gt;OpenAI의 Codex App&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex App은 단순히 코드를 생성하는 도구를 넘어 프로젝트를 분석하고, 필요한 변경 사항을 적용하며, 실제 개발 업무를 수행할 수 있는 AI 개발 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프로젝트 규모가 커질수록 다음과 같은 고민이 생깁니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전 작업 내용을 계속 기억했으면 좋겠다.&lt;/li&gt;
&lt;li&gt;여러 프로젝트를 한 번에 관리하고 싶다.&lt;/li&gt;
&lt;li&gt;GitHub나 Telegram과 연동해 자동화하고 싶다.&lt;/li&gt;
&lt;li&gt;반복적인 작업은 AI가 대신 처리했으면 좋겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 역할을 수행하는 것이 &lt;b&gt;Hermes&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Hermes가 무엇인지, Codex와 어떤 차이가 있는지, 그리고 왜 함께 사용하는 것이 좋은지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AI 개발 방식의 변화&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기의 AI 개발 방식은 대부분 다음과 같았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3227&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PQ6LW/dJMcagNjGx8/0UTlzKLhtsYtPammzSti30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PQ6LW/dJMcagNjGx8/0UTlzKLhtsYtPammzSti30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PQ6LW/dJMcagNjGx8/0UTlzKLhtsYtPammzSti30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPQ6LW%2FdJMcagNjGx8%2F0UTlzKLhtsYtPammzSti30%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;3227&quot; height=&quot;305&quot; data-origin-width=&quot;3227&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 코드를 생성하지만, 프로젝트를 기억하거나 작업 흐름을 관리하지는 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 개발자는 현재 진행중인 작업이나 이전에 수정했던 코드, 앞으로 해야 할 작업들을 모두 직접 관리해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 이러한 방식이 AI Agent 중심으로 변화하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3001&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWV8e1/dJMcacRwLFB/6HvhxgxkDAiRntKlCey2kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWV8e1/dJMcacRwLFB/6HvhxgxkDAiRntKlCey2kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWV8e1/dJMcacRwLFB/6HvhxgxkDAiRntKlCey2kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWV8e1%2FdJMcacRwLFB%2F6HvhxgxkDAiRntKlCey2kK%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;3001&quot; height=&quot;780&quot; data-origin-width=&quot;3001&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 단순히 코드를 생성하는 것이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 이해하고 작업을 수행하는 방향으로 발전하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Hermes란?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes는 Nous Research에서 개발 중인 &lt;b&gt;오픈소스 AI Agent 플랫폼&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes 자체가 뛰어난 LLM이라기보다는, 여러 AI 모델과 외부 도구를 연결하고 관리하는 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 상태를 확인하고&lt;/li&gt;
&lt;li&gt;장기 기억(Memory)을 관리하며&lt;/li&gt;
&lt;li&gt;여러 AI에게 작업을 분배하고&lt;/li&gt;
&lt;li&gt;외부 시스템과 연결하는&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 오케스트레이터(Orchestrator)입니다.&lt;/p&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Hermes가 하는 일&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes는 다양한 기능을 하나의 플랫폼에서 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5088&quot; data-origin-height=&quot;1255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba6HEI/dJMcac468D8/AvrSmQlkKoTWckKxswP9k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba6HEI/dJMcac468D8/AvrSmQlkKoTWckKxswP9k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba6HEI/dJMcac468D8/AvrSmQlkKoTWckKxswP9k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba6HEI%2FdJMcac468D8%2FAvrSmQlkKoTWckKxswP9k0%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;5088&quot; height=&quot;1255&quot; data-origin-width=&quot;5088&quot; data-origin-height=&quot;1255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;장기 Memory 관리&lt;/li&gt;
&lt;li&gt;AI Skill 실행&lt;/li&gt;
&lt;li&gt;프로젝트 관리&lt;/li&gt;
&lt;li&gt;GitHub 연동&lt;/li&gt;
&lt;li&gt;Telegram 연동&lt;/li&gt;
&lt;li&gt;Browser Automation&lt;/li&gt;
&lt;li&gt;Codex 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes는 프로젝트 전체를 관리하는 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그렇다면 Codex는?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex는 실제 개발 업무를 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 코드 작성 및 수정, 버그 수정, 테스트 코드 생성, 프로젝트 분석, 리팩터링 등은 Codex가 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes가 작업을 계획한다면, Codex는 실제 구현을 담당하는 개발자에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Hermes와 Codex의 역할 비교&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 223px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Codex App&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Hermes Desktop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;코드 작성&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;△&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;코드 수정&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;△&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;프로젝트 분석&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;장기 Memory&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;작업 계획&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;△&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;GitHub 연동&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;△&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Telegram 연동&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;자동화&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;여러 프로젝트 관리&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;✅&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;왜 함께 사용할까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;&quot;이번 주 CVERadar 작업을 진행해 줘.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex만 사용하는 경우에는&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2493&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XAOpV/dJMcag7utQt/1VkI4NwcCDulYdmlCNuAPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XAOpV/dJMcag7utQt/1VkI4NwcCDulYdmlCNuAPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XAOpV/dJMcag7utQt/1VkI4NwcCDulYdmlCNuAPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXAOpV%2FdJMcag7utQt%2F1VkI4NwcCDulYdmlCNuAPK%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;2493&quot; height=&quot;305&quot; data-origin-width=&quot;2493&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 잘 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번 주 작업이 무엇인지, 어떤 이슈가 남아 있는지, 이전에 어떤 작업을 했는지 등은 사용자가 알려줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 Hermes를 함께 사용하면 흐름이 달라집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2437&quot; data-origin-height=&quot;1430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rDUkB/dJMcadbNW4x/8bkgPeEXBNbk7rxdN3hqck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rDUkB/dJMcadbNW4x/8bkgPeEXBNbk7rxdN3hqck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rDUkB/dJMcadbNW4x/8bkgPeEXBNbk7rxdN3hqck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrDUkB%2FdJMcadbNW4x%2F8bkgPeEXBNbk7rxdN3hqck%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;622&quot; height=&quot;365&quot; data-origin-width=&quot;2437&quot; data-origin-height=&quot;1430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 이미지와 같이 Hermes가 프로젝트 상태 확인, Memory 검색, GitHub 확인, Codex 호출, 결과 정리까지 수행하게 되며 AI가 프로젝트 전체를 관리하기 시작하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;언제 Hermes가 필요할까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes는 다음과 같은 경우 특히 효과적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 여러 프로젝트를 동시에 관리하는 경우&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트별 작업 내용과 진행 상황을 기억하고 관리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2. 반복 작업이 많은 경우&lt;/blockquote&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;PR 생성&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;등의 반복 작업을 자동화할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;3. GitHub 중심으로 개발하는 경우&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub와 연동하여&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Issue 확인&lt;/li&gt;
&lt;li&gt;Pull Request 생성&lt;/li&gt;
&lt;li&gt;작업 상태 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 하나의 Agent가 수행할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;4. 원격으로 프로젝트를 관리하고 싶은 경우&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Telegram과 연결하면 휴대폰에서도&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;PR 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 수행할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;추천하는 개발 환경&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;2371&quot; data-origin-height=&quot;1730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccOpBe/dJMcaa0xkUu/yQwqjeLL58K5NGLKaF8VjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccOpBe/dJMcaa0xkUu/yQwqjeLL58K5NGLKaF8VjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccOpBe/dJMcaa0xkUu/yQwqjeLL58K5NGLKaF8VjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccOpBe%2FdJMcaa0xkUu%2FyQwqjeLL58K5NGLKaF8VjK%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;709&quot; height=&quot;517&quot; data-origin-width=&quot;2371&quot; data-origin-height=&quot;1730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 개발은 Codex가 담당하고, Hermes는 프로젝트를 관리하는 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;Codex는 AI 개발자 &lt;/b&gt;&lt;b&gt;Hermes는 AI 프로젝트 매니저&lt;/b&gt;라고 이해하면 가장 쉽습니다.&lt;/p&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex App은 프로젝트를 이해하고 실제 코드를 작성하는 뛰어난 AI 개발 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Hermes는 프로젝트의 흐름을 관리하고, 장기 기억과 자동화를 담당하는 AI Agent 플랫폼입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 규모가 작다면 Codex만으로도 충분할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여러 프로젝트를 동시에 관리하거나, 반복 작업을 자동화하고, GitHub와 같은 외부 시스템을 연동하려는 경우에는 Hermes를 함께 사용하는 것이 훨씬 효율적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 도구는 경쟁하는 관계가 아니라 서로를 보완하는 관계이며, 함께 사용할 때 AI 개발 환경의 활용 범위가 크게 넓어집니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Windows 환경에서 Hermes Desktop을 설치하고 Codex App과 연동하는 과정&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;Hermes 설치&lt;/li&gt;
&lt;li&gt;Hermes Desktop 실행&lt;/li&gt;
&lt;li&gt;Codex App 연동&lt;/li&gt;
&lt;li&gt;첫 번째 AI Agent 실행&lt;/li&gt;
&lt;li&gt;기본 설정 및 권장 구성&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI Engineering/Methodology</category>
      <author>KoreaNirsa</author>
      <guid isPermaLink="true">https://nirsa.tistory.com/495</guid>
      <comments>https://nirsa.tistory.com/495#entry495comment</comments>
      <pubDate>Wed, 5 Aug 2026 10:59:30 +0900</pubDate>
    </item>
    <item>
      <title>[Java GC] Old Generation은 어떻게 수집될까: Parallel GC와 Full GC, 할당 성능</title>
      <link>https://nirsa.tistory.com/494</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Java GC] Old Generation은 어떻게 수집될까: Parallel GC와 Full GC, 할당 성능&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nirsa.tistory.com/493&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 포스팅&lt;/a&gt;에서는 Young Generation에서 객체가 어떻게 할당되고, Young GC에서 살아남은 객체가 Survivor Space를 거쳐 Old Generation으로 승격되는 과정을 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 그다음 단계인 Old Generation을 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Old Generation에는 Young GC에서 여러 번 살아남았거나, Survivor Space에 보관하기 어려워 승격된 객체가 주로 저장됩니다. 이러한 객체는 Young Generation의 객체보다 오래 살아 있을 가능성이 높으므로, Young GC와 같은 복사 방식만으로 설명하기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Old Generation의 수집은 객체 할당 속도와 분리된 문제가 아닙니다. 애플리케이션이 객체를 빠르게 생성하면 Young GC의 발생 빈도가 달라지고, 살아남는 객체가 많다면 Old Generation으로 이동하는 객체도 증가할 수 있습니다.&lt;/p&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;이 글은 JDK 21의 Parallel GC를 중심으로 Old Generation 수집을 설명합니다.&lt;/li&gt;
&lt;li&gt;Parallel GC는 여러 GC 스레드로 Young Generation과 Old Generation을 수집합니다.&lt;/li&gt;
&lt;li&gt;Old Generation 수집에서는 생존 객체를 재배치해 메모리 단편화를 줄입니다.&lt;/li&gt;
&lt;li&gt;Full GC는 일반적으로 Old Generation을 포함한 전체 힙을 대상으로 수행됩니다.&lt;/li&gt;
&lt;li&gt;높은 할당 속도가 반드시 높은 승격량을 의미하지는 않습니다.&lt;/li&gt;
&lt;li&gt;GC 성능은 처리량, 할당 속도, 중단 시간, 전체 처리 시간을 함께 봐야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 글을 시작하기 전에 참고로 해당 포스팅은 Parallel GC를 기준으로 설명 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 21의 일반적인 서버 환경에서는 G1 GC가 기본 수집기로 선택됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;G1 GC는 힙을 동일한 크기의 리전으로 나누며, Concurrent Marking과 Mixed GC를 통해 일부 Old Region을 점진적으로 회수합니다. 따라서 이 글에서 다루는 연속된 Young Generation과 Old Generation, Old Generation 압축 과정은 G1 GC의 전체 동작을 설명하는 내용이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 다음 옵션으로 활성화하는 &lt;b&gt;Parallel GC&lt;/b&gt;를 기준으로 합니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;java -XX:+UseParallelGC -jar application.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC는 처리량 수집기(Throughput Collector)라고도 부르는 세대별 수집기입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young Generation의 Minor Collection과 Old Generation을 포함하는 Major Collection에 여러 GC 스레드를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;JDK 21 HotSpot JVM
        &amp;darr;
-XX:+UseParallelGC
        &amp;darr;
Parallel GC
        ├─ Young Generation 병렬 수집
        └─ Old Generation 병렬 수집
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Old Generation에는 어떤 객체가 저장될까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 살펴본 것처럼 Young GC에서 살아남은 객체는 다른 Survivor로 복사되거나, 일정한 조건에 따라 Old Generation으로 승격될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 승격 조건은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체의 나이가 승격 기준에 도달한 경우&lt;/li&gt;
&lt;li&gt;Survivor 에 생존 객체를 모두 저장하기 어려운 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Old Generation에는 이러한 과정을 거쳐 비교적 오래 살아남은 객체가 주로 모입니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Young Generation
        &amp;darr;
Young GC에서 생존
        &amp;darr;
Survivor에서 계속 생존
        &amp;darr;
승격 조건 충족
        &amp;darr;
Old Generation&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 모든 객체가 반드시 Eden과 Survivor Space를 거쳐 Old Generation으로 이동하는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 크기와 사용 중인 수집기의 정책에 따라 별도의 할당 경로가 사용될 수 있습니다. 예를 들어 G1 GC에서는 리전 크기의 절반 이상인 Humongous Object를 Old Generation에 속하는 Humongous Region에 직접 할당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서 핵심적으로 봐바야 할 부분은 Old Generation에는 Young Generation보다 오래 살아있는 객체가 많을 수 있으며, 이러한 객체를 어떻게 수집하고 공간을 정리할지를 봐야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;※ 리전(Region)&lt;/b&gt;&lt;br /&gt;G1 GC가 Java 힙을 관리하기 위해 나눈 동일한 크기의 메모리 구역입니다. 각 리전은 상황에 따라 Eden, Survivor, Old 등의 용도로 사용됩니다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;※ Humongous Object&lt;/b&gt;&lt;br /&gt;크기가 리전 하나의 50% 이상인 매우 큰 객체입니다. 일반 객체처럼 Young Generation의 Eden에 할당하고 복사하기에는 비용이 크므로 별도로 처리합니다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;※ Humongous Region&lt;/b&gt;&lt;br /&gt;Humongous Object를 저장하기 위해 사용되는 Old Generation 소속의 리전입니다. 객체가 리전 하나보다 크다면 여러 개의 연속된 리전을 사용합니다.&lt;br /&gt;&lt;br /&gt;예를 들어 리전 크기가 4MB라면, 크기가 2MB 이상인 객체는 Humongous Object로 분류되어 Humongous Region에 직접 할당됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Old Generation은 왜 Young Generation과 다르게 수집할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young Generation에는 생성된 지 얼마 되지 않은 객체가 모입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약한 세대 가설에 따라 이러한 객체 중 상당수는 비교적 빠르게 도달 불가능한 상태가 됩니다. 따라서 Young GC에서는 살아 있는 객체만 다른 Survivor Space나 Old Generation으로 복사하고, 기존 Eden과 From Survivor를 한 번에 다시 사용하는 방식이 효율적일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Old Generation에는 여러 번의 GC에서 계속 살아남은 객체가 주로 저장됩니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Young Generation
새 객체가 주로 저장됨
        &amp;darr;
짧은 수명의 객체가 많을 가능성
        &amp;darr;
생존 객체만 복사

# Old Generation
오래 살아남은 객체가 주로 저장됨
        &amp;darr;
생존 객체 비율이 높을 가능성
        &amp;darr;
생존 객체 재배치와 공간 압축&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Old Generation에서 살아 있는 객체가 많다면 모든 생존 객체를 별도의 빈 공간으로 복사하기 위해 큰 추가 공간이 필요할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Parallel GC의 Old Generation 수집에서는 생존 객체를 같은 세대 안에서 재배치하고, 분산된 빈 공간을 하나로 모으는 압축 과정이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;병렬 컬렉션이란&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 컬렉션은 여러 GC 스레드가 하나의 가비지 컬렉션 작업을 나누어 수행하는 방식입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3576&quot; data-origin-height=&quot;1255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDvsS/dJMcaaTIias/i00LKKeJ1C2srKxroVpfm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDvsS/dJMcaaTIias/i00LKKeJ1C2srKxroVpfm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDvsS/dJMcaaTIias/i00LKKeJ1C2srKxroVpfm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDvsS%2FdJMcaaTIias%2Fi00LKKeJ1C2srKxroVpfm1%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;3576&quot; height=&quot;1255&quot; data-origin-width=&quot;3576&quot; data-origin-height=&quot;1255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 CPU 코어를 사용할 수 있는 환경에서는 한 개의 스레드가 모든 작업을 수행하는 것보다 수집 시간을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 21의 Parallel GC는 여러 GC 스레드를 사용해 수집 작업을 수행하며, CPU가 하나뿐인 환경에서는 병렬 처리의 동기화 비용 때문에 Serial GC보다 불리할 수 있습니다. 반대로 여러 CPU와 중대형 힙을 사용하는 환경에서는 Serial GC보다 높은 처리량을 기대할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 수집과 동시 수집은 서로 다른 개념입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;병렬(Parallel)&lt;/td&gt;
&lt;td&gt;여러 GC 스레드가 수집 작업을 나누어 수행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동시(Concurrent)&lt;/td&gt;
&lt;td&gt;GC 작업 일부가 애플리케이션 실행과 겹쳐 수행됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC의 주요 수집 작업은 STW(Stop-The-World) 상태에서 이루어집니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;애플리케이션 실행
    &amp;darr;
STW 시작
    &amp;darr;
애플리케이션 스레드 정지
    &amp;darr;
여러 GC 스레드가 병렬로 수집
    &amp;darr;
STW 종료
    &amp;darr;
애플리케이션 실행 재개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Parallel GC에서의 병렬은 &lt;b&gt;애플리케이션과 GC가 동시에 실행된다는 의미가 아니라, 여러 GC 스레드가 함께 수집한다는 의미&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Parallel Old와 Serial Old는 어떻게 이해해야 할까&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨부된 내용에서는 Old Generation을 수집하는 방식으로 Parallel Old와 Serial Old를 구분합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명칭은 HotSpot GC의 역사와 내부 구성을 이해할 때 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel Old는 여러 GC 스레드를 사용해 Old Generation을 수집하고 압축하는 방식을 가리킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Serial Old는 하나의 GC 스레드가 Old Generation을 수집하고 압축하는 방식을 가리킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;b&gt;JDK 21에서는 두 방식을 독립적인 선택지처럼 이해하면 안 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 Parallel Scavenge + Serial Old 조합을 사용할 수 있었지만, 이 조합은 &lt;a href=&quot;https://openjdk.org/jeps/366&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JDK 14에서 deprecated&lt;/a&gt; 되었고 관련 UseParallelOldGC 옵션은 JDK 15에서 obsolete 처리되었습니다. JDK 21에서는 -XX:+UseParallelGC로 Parallel GC 전체를 선택하며 Minor Collection과 Major Collection 모두 병렬로 수행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 JDK 21에서는 다음과 같이 이해하는 것이 적절합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;Serial GC&lt;/td&gt;
&lt;td&gt;Parallel GC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;활성화 옵션&lt;/td&gt;
&lt;td&gt;-XX:+UseSerialGC&lt;/td&gt;
&lt;td&gt;-XX:+UseParallelGC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Young Generation 수집&lt;/td&gt;
&lt;td&gt;단일 GC 스레드&lt;/td&gt;
&lt;td&gt;여러 GC 스레드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old Generation 수집&lt;/td&gt;
&lt;td&gt;단일 GC 스레드&lt;/td&gt;
&lt;td&gt;여러 GC 스레드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주요 목적&lt;/td&gt;
&lt;td&gt;작은 힙과 제한된 CPU 환경&lt;/td&gt;
&lt;td&gt;다중 코어 환경의 높은 처리량&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STW&lt;/td&gt;
&lt;td&gt;발생합니다.&lt;/td&gt;
&lt;td&gt;발생합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old Generation 압축&lt;/td&gt;
&lt;td&gt;수행합니다.&lt;/td&gt;
&lt;td&gt;수행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel Old는 JDK 21에서 별도로 선택하는 GC 이름이라기보다, &lt;b&gt;Parallel GC의 Old Generation 수집 부분을 설명하는 명칭&lt;/b&gt;으로 보는 편이 정확합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Old Generation에서는 왜 압축이 필요할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Old Generation에서 도달 불가능한 객체가 사용하던 공간만 비우면 메모리 곳곳에 작은 빈 공간이 남을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 수집 전
[객체 A][가비지][객체 B][가비지][객체 C]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 객체가 사용하던 공간을 회수하면 다음과 같이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 공간 회수 후
[객체 A][빈 공간][객체 B][빈 공간][객체 C]&lt;/code&gt;&lt;/pre&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;빈 공간 2MB + 빈 공간 3MB + 빈 공간 5MB&lt;/li&gt;
&lt;li&gt;새 객체 크기 6MB&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 상황일 때 6MB를 가진 공간이 없기 때문에 즉시 할당이 어려워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 사용 가능한 공간이 여러 위치에 흩어져 큰 연속 공간을 확보하기 어려운 상태를 &lt;b&gt;외부 단편화(External&amp;nbsp;Fragmentation)&lt;/b&gt;라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC의 Old Generation 수집에서는 살아 있는 객체를 재배치하여 빈 공간을 통합합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 압축 전
[객체 A][빈 공간][객체 B][빈 공간][객체 C]

// 압축 후
[객체 A][객체 B][객체 C][ 연속된 빈 공간 ]&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;4365&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHFvi5/dJMcahrP65p/Moickfo1qrrKrsOjT3UMkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHFvi5/dJMcahrP65p/Moickfo1qrrKrsOjT3UMkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHFvi5/dJMcahrP65p/Moickfo1qrrKrsOjT3UMkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHFvi5%2FdJMcahrP65p%2FMoickfo1qrrKrsOjT3UMkK%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;4365&quot; height=&quot;305&quot; data-origin-width=&quot;4365&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 이동하면 해당 객체를 가리키는 참조도 새로운 위치를 가리키도록 수정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 압축은 메모리 단편화를 줄이는 장점이 있지만, 생존 객체를 이동하고 참조를 갱신하는 비용이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC의 Old Generation 수집 알고리즘은 구현 관점에서 Mark-Summary-Compact 계열로 설명됩니다. 반드시 Mark &amp;rarr; Sweep &amp;rarr; Compact라는 세 개의 독립 단계가 실행된다고 이해하기보다 아래와 같은 흐름으로 이해하는 편이 좋습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;생존 객체 식별&lt;/li&gt;
&lt;li&gt;생존 객체 재배치&lt;/li&gt;
&lt;li&gt;분산된 빈 공간 통합&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Full GC란 무엇일까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Full GC는 일반적으로 &lt;b&gt;Young Generation만이 아니라&lt;/b&gt; &lt;b&gt;Old Generation을 포함한 전체 Java Heap을 대상으로 수행되는 수집&lt;/b&gt;을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC에서 Full GC가 발생하면 애플리케이션 스레드를 멈춘 상태에서 전체 힙의 생존 객체를 처리하고, Old Generation의 객체를 재배치해 공간을 압축합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;7857&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4tkKW/dJMcaiKVmpW/JR8XmIpxQGJIe6j6dOmDK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4tkKW/dJMcaiKVmpW/JR8XmIpxQGJIe6j6dOmDK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4tkKW/dJMcaiKVmpW/JR8XmIpxQGJIe6j6dOmDK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4tkKW%2FdJMcaiKVmpW%2FJR8XmIpxQGJIe6j6dOmDK0%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;7857&quot; height=&quot;305&quot; data-origin-width=&quot;7857&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Full GC는 Young GC보다 일반적으로 처리 범위가 넓으며&amp;nbsp;Old Generation에는 살아 있는 객체가 많이 존재할 수 있으므로 객체 이동과 참조 갱신 비용도 커질 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 122px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Young GC&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Full GC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;주요 수집 대상&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Young Generation&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;전체 Java Heap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Old Generation 처리&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;일반적인 주 대상이 아님&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;주요 작업&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;생존 객체 복사와 승격&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;전체 힙 처리와 Old Generation 압축&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;STW&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Parallel GC에서는 발생&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;일반적인 비용&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;상대적으로 작음&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;상대적으로 큼&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Major GC와 Full GC&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Major GC는 문서나 분석 도구에 따라 의미가 다르게 사용될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 자료에서는 Old Generation을 수집하는 작업을 Major GC라고 부르고, 다른 자료에서는 Full GC와 유사한 의미로 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Minor GC : 일반적으로 Young Generation 수집&lt;/li&gt;
&lt;li&gt;Major GC : 일반적으로 Old Generation의 수집을 의미하지만 구현체나 자료에 따라 Old Generation만 수집하는 경우 또는 전체 힙을 수집하는 경우를 가리킬 수 있음&lt;/li&gt;
&lt;li&gt;Full GC : 일반적으로 전체 힙을 대상으로 수집&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 JDK 21 Parallel GC 문서는 Minor Collection과 Major Collection이 모두 병렬로 수행된다고 설명합니다. 실제 운영에서는 Major GC라는 이름만으로 수집 범위를 단정하지 말고 GC 로그에 표시된 이벤트와 수집기를 함께 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Full GC는 언제 발생할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Full GC는 Old Generation이 정확히 100% 찼을 때만 발생하는 작업이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC에서는 다음과 같은 상황과 연결될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;1. 승격할 공간을 확보하지 못한 경우&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young GC에서 살아남은 객체를 Old Generation으로 승격해야 하지만 충분한 공간을 확보하지 못하면 Full GC가 수행될 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5595&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6UxaI/dJMcaiEeU3W/LT2yzRqoDspTNWwmJBsKYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6UxaI/dJMcaiEeU3W/LT2yzRqoDspTNWwmJBsKYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6UxaI/dJMcaiEeU3W/LT2yzRqoDspTNWwmJBsKYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6UxaI%2FdJMcaiEeU3W%2FLT2yzRqoDspTNWwmJBsKYk%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;5595&quot; height=&quot;872&quot; data-origin-width=&quot;5595&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;2. 객체 할당에 필요한 공간이 부족한 경우&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young Generation과 Old Generation에서 필요한 공간을 확보하지 못하면 JVM은 Full GC를 통해 공간을 회수하려고 시도할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;6200&quot; data-origin-height=&quot;1172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwoz39/dJMcaiqEO58/JfK5xK283X4fA5DhKMI1Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwoz39/dJMcaiqEO58/JfK5xK283X4fA5DhKMI1Hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwoz39/dJMcaiqEO58/JfK5xK283X4fA5DhKMI1Hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwoz39%2FdJMcaiqEO58%2FJfK5xK283X4fA5DhKMI1Hk%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;6200&quot; height=&quot;1172&quot; data-origin-width=&quot;6200&quot; data-origin-height=&quot;1172&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;3. 명시적인 GC가 요청된 경우&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 System.gc()를 호출하면 JVM에 GC 수행을 요청합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;System.gc();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;System.gc()는 명세상 GC 실행을 보장하는 명령이 아니라 요청입니다. 하지만 일반적인 HotSpot JVM에서는 Full GC의 원인이 될 수 있으므로 애플리케이션 코드에서 직접 호출하는 것은 피하는 편이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;4. Metaspace 사용량이 높아진 경우&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 메타데이터가 저장되는 Metaspace의 사용량이 기준에 도달하면 클래스 언로딩과 메타데이터 회수를 위해 GC가 유도될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Metaspace는 Java Heap이 아니라 Native Memory에 존재합니다. 따라서 Full GC를 단순히 &amp;ldquo;Java Heap 공간이 부족할 때만 발생하는 작업&amp;rdquo;으로 이해하면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Full GC가 발생하면 무조건 문제일까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Full GC가 한 번 발생했다는 사실만으로 애플리케이션에 문제가 있다고 단정할 수는 없습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Full GC가 짧은 간격으로 반복됨&lt;/li&gt;
&lt;li&gt;FUll GC 중단 시간이 계속 증가함&lt;/li&gt;
&lt;li&gt;Full GC 이후에도 Gold Generation 사용량이 거의 줄지 않음&lt;/li&gt;
&lt;li&gt;Old Generation 사용량이 지속적으로 증가함&lt;/li&gt;
&lt;li&gt;GC에 대부분의 실행 시간을 사용함&lt;/li&gt;
&lt;li&gt;Full GC 이후 OutOfMemoryError가 발생함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Full GC 이후에도 Old Generation의 사용량이 크게 줄지 않는다면 살아 있는 객체의 총량인 Live Set이 크거나, 의도하지 않은 참조가 객체를 계속 유지하는 메모리 누수를 의심할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Full GC 이전 Old Generation 90%
        &amp;darr; Full GC
Full GC 이후 Old Generation 85%
        &amp;darr;
대부분의 객체가 여전히 생존
        &amp;darr;
높은 Live Set 또는 메모리 누수 가능성 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Full GC를 분석할 때는 발생 횟수만 볼 것이 아니라 다음 항목을 함께 확인해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Full GC 발생 원인&lt;/li&gt;
&lt;li&gt;Full GC 전후의 힙 사용량&lt;/li&gt;
&lt;li&gt;Full GC 중단 시간&lt;/li&gt;
&lt;li&gt;Full GC 발생 간격&lt;/li&gt;
&lt;li&gt;Old Generation 증가 속도&lt;/li&gt;
&lt;li&gt;객체 할당 속도&lt;/li&gt;
&lt;li&gt;객체 생존율&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체 할당 속도와 GC는 어떻게 연결될까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nirsa.tistory.com/493&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서는 TLAB과 범프 포인터 할당을 통해 객체가 빠르게 만들어지는 구조를 살펴봤습니다. 이번 글에서는 할당의 세부 방식보다 &lt;b&gt;객체가 생성되는 속도와 GC 사이의 관계&lt;/b&gt;를 작성하려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 객체를 빠르게 생성하면 Young Generation의 여유 공간도 빠르게 줄어듭니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4789&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlfZrk/dJMcaasHe1C/URCAjkIqvyfHm57CzK3pXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlfZrk/dJMcaasHe1C/URCAjkIqvyfHm57CzK3pXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlfZrk/dJMcaasHe1C/URCAjkIqvyfHm57CzK3pXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlfZrk%2FdJMcaasHe1C%2FURCAjkIqvyfHm57CzK3pXK%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;4789&quot; height=&quot;410&quot; data-origin-width=&quot;4789&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨부 범위의 ModelAllocator 예시는 여러 스레드가 각자의 할당 영역에서 객체를 만들고, 할당자가 새로운 객체 생성을 조정하는 구조를 보여 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시의 핵심은 특정 구현 코드 자체보다 다음 관계에 있습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;애플리케이션 스레드
        &amp;darr;
객체 할당 요청
        &amp;darr;
할당 가능한 공간 소비
        &amp;darr;
할당 공간 부족
        &amp;darr;
GC를 통한 공간 회수
&lt;/code&gt;&lt;/pre&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;할당 속도가 높으면 Old Generation도 빠르게 찰까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 그렇지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할당 속도가 높아도 생성된 객체 대부분이 Young GC 전에 도달 불가능한 상태가 된다면 Old Generation으로 승격되는 객체는 많지 않을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;높은 할당 속도
        +
낮은 객체 생존율
        &amp;darr;
Young GC는 자주 발생할 수 있음
        &amp;darr;
Old Generation 승격량은 적을 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 할당 속도와 생존율이 모두 높다면 Survivor Space에 복사되는 객체와 Old Generation으로 승격되는 객체가 증가할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;높은 할당 속도
        +
높은 객체 생존율
        &amp;darr;
Young GC 빈도 증가 가능
        &amp;darr;
Survivor 복사량 증가
        &amp;darr;
Old Generation 승격량 증가
        &amp;darr;
Full GC 압력 증가 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적인 관계는 다음처럼 표현할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Old Generation 승격 압력 &amp;asymp; 객체 할당 속도 &amp;times; Young GC 생존율&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 JVM에서 사용하는 실제 계산 공식은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할당 속도만으로 Old Generation의 증가량을 판단해서는 안 되며, Young GC에서 살아남는 객체의 비율도 함께 확인해야 한다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Parallel GC의 승격 버퍼와 단편화&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC에서는 Young GC에 여러 GC 스레드가 참여합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 GC 스레드는 생존 객체를 Old Generation으로 승격하기 위해 Old Generation의 일부 공간을 예약할 수 있습니다. Oracle의 JDK 21 문서는 이 공간을 promotion buffer로 설명하며, 여러 GC 스레드가 공간을 나누어 예약하는 과정에서 단편화 효과가 발생할 수 있다고 설명합니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;Old Generation

[GC 스레드 1의 승격 버퍼]
[GC 스레드 2의 승격 버퍼]
[GC 스레드 3의 승격 버퍼]
[남은 공간]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Parallel GC에서는 Old Generation 단편화를 단순히 &amp;ldquo;가비지 객체를 제거한 뒤 빈 공간이 흩어지는 현상&amp;rdquo;만으로 볼 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 승격을 위해 여러 GC 스레드가 공간을 나누어 사용하는 과정도 Old Generation의 공간 활용에 영향을 줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle 문서는 이러한 단편화 효과를 줄이는 방법으로 GC 스레드 수를 줄이거나 Old Generation의 크기를 늘리는 방향을 언급합니다. 하지만 실제 환경에서는 GC 스레드 수를 임의로 줄이기보다 GC 로그와 부하 테스트를 통해 전체 처리량과 중단 시간을 함께 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GC 성능은 네 가지 지표를 함께 봐야 합니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨부된 내용에서는 GC 성능을 다음 네 가지 요소로 설명합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;성능 요소&lt;/td&gt;
&lt;td&gt;살펴보는 관점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;처리량&lt;/td&gt;
&lt;td&gt;일정 시간 동안 애플리케이션이 수행한 작업량&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;할당 속도&lt;/td&gt;
&lt;td&gt;일정 시간 동안 객체에 할당한 메모리의 양&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중단 시간&lt;/td&gt;
&lt;td&gt;GC로 애플리케이션 스레드가 멈춘 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전체 처리 시간&lt;/td&gt;
&lt;td&gt;애플리케이션의 작업이 완료될 때까지 걸린 시간&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;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;3097&quot; data-origin-height=&quot;1730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVjF5j/dJMcafANPTW/Kj8VyYtoIBNRTWku7p71hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVjF5j/dJMcafANPTW/Kj8VyYtoIBNRTWku7p71hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVjF5j/dJMcafANPTW/Kj8VyYtoIBNRTWku7p71hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVjF5j%2FdJMcafANPTW%2FKj8VyYtoIBNRTWku7p71hk%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;656&quot; height=&quot;366&quot; data-origin-width=&quot;3097&quot; data-origin-height=&quot;1730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 처리량&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리량은 전체 실행 시간 중 애플리케이션 작업에 사용된 시간의 비율로 표현할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;처리량
=
애플리케이션 실행 시간
────────────────────────────
애플리케이션 실행 시간 + GC 시간
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 애플리케이션이 99초 동안 작업하고 GC로 1초 동안 중단되었다면 처리량은 약 99%입니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;애플리케이션 실행 시간: 99초
GC 시간: 1초
전체 실행 시간: 100초

처리량
= 99 / 100
= 99%
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC에서는 -XX:GCTimeRatio 옵션으로 처리량 목표를 지정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-XX:GCTimeRatio=19
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCTimeRatio=19는 전체 시간의 약 5%를 GC에 사용하는 목표를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC의 기본값은 99이며, 전체 시간의 약 1%를 GC에 사용하는 것을 목표로 합니다. 이는 반드시 달성되는 보장이 아니라 JVM이 달성하려고 시도하는 목표입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2. 할당 속도&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할당 속도는 일정 시간 동안 애플리케이션이 객체에 할당한 메모리의 양을 의미합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;MB/s
GB/s
objects/s
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할당 속도가 높으면 Young Generation이 빠르게 소모되어 Young GC가 자주 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Old Generation의 증가 속도를 판단하려면 객체 생존율과 승격량도 함께 확인해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Allocation Rate
        &amp;darr;
Young GC 빈도에 영향

Survival Rate
        &amp;darr;
Survivor 복사량과 승격량에 영향

Promotion Rate
        &amp;darr;
Old Generation 증가 속도에 영향
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;3. 중단 시간&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중단 시간은 GC로 인해 애플리케이션 스레드가 실행되지 못하는 시간입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC는 주요 수집 작업을 STW 상태에서 수행하므로 GC 중단 시간이 사용자 응답 시간에 영향을 줄 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3295&quot; data-origin-height=&quot;2755&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vmV34/dJMcaf8xIVA/HcCio8COWQkNFOcPKvlB7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vmV34/dJMcaf8xIVA/HcCio8COWQkNFOcPKvlB7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vmV34/dJMcaf8xIVA/HcCio8COWQkNFOcPKvlB7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvmV34%2FdJMcaf8xIVA%2FHcCio8COWQkNFOcPKvlB7k%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;620&quot; height=&quot;518&quot; data-origin-width=&quot;3295&quot; data-origin-height=&quot;2755&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 300ms의 GC 중단이 발생했다고 해서 모든 요청의 응답 시간이 정확히 300ms씩 증가하는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청의 처리 구간이 STW 구간과 얼마나 겹치는지에 따라 실제 지연은 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 평균값뿐 아니라 최대값과 백분위 지표도 함께 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;평균 GC Pause Time
최대 GC Pause Time
p95 GC Pause Time
p99 GC Pause Time
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;p95와 p99는 JVM이 직접 사용하는 GC 목표값이라기보다, GC 로그나 JFR에서 수집한 중단 시간을 모니터링 시스템에서 집계한 지표입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;4. 전체 처리 시간&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 처리 시간은 하나의 작업을 시작한 시점부터 완료할 때까지 걸린 시간을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서비스에서는 개별 요청의 지연 시간이 중요할 수 있지만, 배치 애플리케이션에서는 각 GC의 중단 시간보다 전체 작업 완료 시간이 더 중요할 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;설정&lt;/td&gt;
&lt;td&gt;최대 GC 중단 시간&lt;/td&gt;
&lt;td&gt;전체 작업 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설정 A&lt;/td&gt;
&lt;td&gt;100ms&lt;/td&gt;
&lt;td&gt;15분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설정 B&lt;/td&gt;
&lt;td&gt;500ms&lt;/td&gt;
&lt;td&gt;10분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 응답이 중요한 서비스라면 설정 A가 더 적합할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 야간 배치 작업이라면 개별 중단이 길더라도 전체 작업을 더 빠르게 끝내는 설정 B가 적합할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Parallel GC가 사용하는 성능 목표&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 관점에서는 처리량, 할당 속도, 중단 시간, 전체 처리 시간을 함께 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 별도로 Parallel GC의 적응형 정책은 다음 세 가지 목표를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 최대 중단 시간
        &amp;darr;
2. 처리량
        &amp;darr;
3. 최소 힙 Footprint
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC는 최대 중단 시간 목표를 먼저 고려하고, 해당 목표를 만족한 뒤 처리량 목표를 확인합니다. 두 목표를 모두 만족한 경우 힙 Footprint를 줄이는 방향을 고려합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;※ Footprint : 자바 애플리케이션이 실행되는 동안 OS로부터 할당받아 사용하는 총 물리 메모리 양&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parallel GC 목표&lt;/td&gt;
&lt;td&gt;관련 옵션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최대 중단 시간&lt;/td&gt;
&lt;td&gt;-XX:MaxGCPauseMillis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;처리량&lt;/td&gt;
&lt;td&gt;-XX:GCTimeRatio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최대 힙 크기&lt;/td&gt;
&lt;td&gt;-Xmx&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MaxGCPauseMillis는 반드시 지켜지는 제한이 아니라 목표에 가까운 힌트입니다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;힙을 크게 설정하면 성능이 좋아질까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙이 커지면 객체를 할당할 수 있는 공간이 늘어나므로 GC 발생 간격이 길어질 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;힙 크기 증가
        &amp;darr;
할당 가능한 공간 증가
        &amp;darr;
GC 발생 빈도 감소 가능
        &amp;darr;
처리량 증가 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 힙이 커졌다고 모든 성능 지표가 좋아지는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC에서 Full GC가 발생하면 더 넓은 메모리 영역과 더 많은 생존 객체를 처리해야 할 수 있으므로 한 번의 중단 시간이 길어질 가능성이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 힙을 지나치게 작게 설정하면 GC가 자주 발생하고 애플리케이션이 실제 작업보다 GC에 더 많은 시간을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 큰 힙
장점
&amp;rarr; GC 발생 간격 증가 가능
&amp;rarr; 처리량 증가 가능

주의점
&amp;rarr; Full GC 처리량 증가 가능
&amp;rarr; 긴 STW 가능
&amp;rarr; 메모리 사용량 증가


//작은 힙
장점
&amp;rarr; 메모리 Footprint 감소

주의점
&amp;rarr; GC 빈도 증가
&amp;rarr; 승격 공간 부족 가능
&amp;rarr; 처리량 감소 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 21의 Parallel GC는 전체 실행 시간의 98% 이상을 GC에 사용하면서 힙의 2% 미만만 회수하는 상태가 계속되면 OutOfMemoryError: GC overhead limit exceeded를 발생시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GC 스레드는 많을수록 좋을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC에서 사용하는 병렬 GC 스레드 수는 다음 옵션으로 조정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-XX:ParallelGCThreads=N
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC 스레드를 늘리면 수집 작업을 더 많이 분할할 수 있지만 항상 성능이 좋아지는 것은 아닙니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// GC 스레드 증가
기대 효과
&amp;rarr; GC 병렬 처리량 증가 가능
&amp;rarr; GC 중단 시간 감소 가능

주의점
&amp;rarr; 동기화 비용 증가
&amp;rarr; CPU 경쟁 증가
&amp;rarr; 애플리케이션이 사용할 CPU 감소
&amp;rarr; 승격 버퍼 단편화 증가 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM은 사용 가능한 하드웨어 스레드를 기준으로 GC 스레드 수를 결정합니다. 단일 CPU 환경에서는 병렬 처리 오버헤드 때문에 Serial GC보다 불리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 GC 스레드 수를 임의로 늘리기보다 실제 GC 로그와 부하 테스트를 바탕으로 조정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Parallel GC는 어떤 애플리케이션에 적합할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC는 긴 중단 시간을 어느 정도 허용할 수 있고, 일정 시간 동안 최대한 많은 작업을 처리하는 것이 중요한 환경에서 고려할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배치 처리&lt;/li&gt;
&lt;li&gt;데이터 분석&lt;/li&gt;
&lt;li&gt;파일 변환&lt;/li&gt;
&lt;li&gt;과학 계산&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;Oracle의 JDK 21 가이드는 최대 처리량이 최우선이며 1초 이상의 GC 중단을 허용할 수 있다면 Parallel GC를 선택 기준 중 하나로 고려할 수 있다고 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 사용자 요청의 지연 시간이 중요한 웹 애플리케이션이나 API 서버에서는 평균 처리량뿐 아니라 최대 GC 중단 시간과 응답 시간의 p95&amp;middot;p99를 함께 확인해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 189px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;확인 항목&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;확인할 질문&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;CPU 코어 수&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;병렬 GC에 사용할 CPU가 충분한가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;힙 크기&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Full GC에서 처리할 메모리 범위가 얼마나 큰가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Live Set&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;GC 이후에도 살아남는 객체의 크기가 얼마나 되는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;할당 속도&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Young Generation이 얼마나 빠르게 소모되는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;생존율&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Young GC에서 얼마나 많은 객체가 살아남는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;승격 속도&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Old Generation이 얼마나 빠르게 증가하는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;허용 중단 시간&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;서비스가 허용할 수 있는 최대 지연은 얼마인가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;전체 처리 시간&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;작업 완료 시간이 중요한가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체 할당부터 Full GC까지의 전체 흐름&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;8192&quot; data-origin-height=&quot;1327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7hPKP/dJMcaalSaJl/Bs70poVzw6quKfyl7xoZ9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7hPKP/dJMcaalSaJl/Bs70poVzw6quKfyl7xoZ9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7hPKP/dJMcaalSaJl/Bs70poVzw6quKfyl7xoZ9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7hPKP%2FdJMcaalSaJl%2FBs70poVzw6quKfyl7xoZ9k%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;8192&quot; height=&quot;1327&quot; data-origin-width=&quot;8192&quot; data-origin-height=&quot;1327&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\성능 관점에서는 다음 흐름으로 연결됩니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;객체 할당 속도
        &amp;darr;
Young Generation 소모 속도
        &amp;darr;
Young GC 발생 빈도
        &amp;darr;
객체 생존율
        &amp;darr;
Survivor 복사량과 승격량
        &amp;darr;
Old Generation 사용량
        &amp;darr;
Full GC 발생 가능성
        &amp;darr;
중단 시간과 전체 처리량 변화
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;자주 혼동하는 부분&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. JDK 21은 Parallel GC를 기본으로 사용할까&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 21의 일반적인 서버 환경에서는 G1 GC가 기본으로 선택됩니다. Parallel GC를 사용하려면 -XX:+UseParallelGC 옵션을 지정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2. Parallel Old는 별도로 선택하는 GC일까&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 21에서는 그렇지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel Old는 Parallel GC의 Old Generation 수집 부분을 설명하는 역사적&amp;middot;구현적 명칭입니다. JDK 21에서는 -XX:+UseParallelGC로 Parallel GC 전체를 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;3. 병렬 수집은 애플리케이션과 동시에 실행될까&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 그렇지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC는 애플리케이션 스레드를 멈춘 뒤 여러 GC 스레드가 수집 작업을 병렬로 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;4. Old Generation은 오래된 객체를 의미할까&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히는 객체의 생성 시각이 오래되었다는 뜻보다, GC에서 여러 번 살아남거나 승격 조건을 만족한 객체가 저장되는 세대를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;4. 높은 할당 속도는 반드시 Old Generation을 빠르게 채울까&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;높은 할당 속도는 Young GC 빈도를 높일 수 있지만, 생성된 객체가 대부분 빠르게 사라진다면 Old Generation 승격량은 적을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;5. Full GC는 Old Generation만 수집할까&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 그렇지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Full GC는 Young Generation과 Old Generation을 포함한 전체 Java Heap을 대상으로 하는 수집으로 이해하는 것이 적절합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;6. Full GC가 한 번 발생하면 메모리 누수일까&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Full GC의 발생 간격, 중단 시간, GC 전후의 힙 사용량을 함께 확인해야 합니다. Full GC 이후에도 Old Generation 사용량이 거의 줄지 않고 계속 증가한다면 높은 Live Set이나 메모리 누수를 점검해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핵심 정리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 글은 JDK 21의 Parallel GC를 기준으로 Old Generation 수집을 설명했습니다.&lt;/li&gt;
&lt;li&gt;JDK 21의 일반적인 서버 환경에서는 G1 GC가 기본으로 선택됩니다.&lt;/li&gt;
&lt;li&gt;Parallel GC는 -XX:+UseParallelGC로 활성화합니다.&lt;/li&gt;
&lt;li&gt;Parallel GC는 Young Generation과 Old Generation의 수집에 여러 GC 스레드를 사용합니다.&lt;/li&gt;
&lt;li&gt;Parallel GC의 주요 수집 작업은 STW 상태에서 수행됩니다.&lt;/li&gt;
&lt;li&gt;Parallel Old는 JDK 21에서 별도로 선택하는 GC가 아니라 Parallel GC의 Old Generation 수집 부분을 가리키는 명칭입니다.&lt;/li&gt;
&lt;li&gt;Old Generation에는 Young GC에서 살아남아 승격된 객체가 주로 저장됩니다.&lt;/li&gt;
&lt;li&gt;Old Generation 수집에서는 생존 객체를 재배치하여 메모리 단편화를 줄입니다.&lt;/li&gt;
&lt;li&gt;압축 과정에서는 객체 이동과 참조 갱신 비용이 발생합니다.&lt;/li&gt;
&lt;li&gt;Full GC는 일반적으로 Young Generation과 Old Generation을 포함한 전체 힙을 대상으로 수행됩니다.&lt;/li&gt;
&lt;li&gt;Full GC는 처리 범위가 넓고 생존 객체가 많을 수 있어 중단 시간이 길어질 가능성이 있습니다.&lt;/li&gt;
&lt;li&gt;높은 객체 할당 속도는 Young GC의 빈도를 높일 수 있습니다.&lt;/li&gt;
&lt;li&gt;Old Generation 승격량은 할당 속도뿐 아니라 객체 생존율에도 영향을 받습니다.&lt;/li&gt;
&lt;li&gt;Parallel GC에서는 병렬 승격을 위한 promotion buffer가 Old Generation 단편화에 영향을 줄 수 있습니다.&lt;/li&gt;
&lt;li&gt;GC 성능은 처리량, 할당 속도, 중단 시간, 전체 처리 시간을 함께 확인해야 합니다.&lt;/li&gt;
&lt;li&gt;Parallel GC의 적응형 정책은 최대 중단 시간, 처리량, 힙 Footprint 순으로 목표를 고려합니다.&lt;/li&gt;
&lt;li&gt;GC 횟수가 적거나 한 번의 수집 시간이 짧다는 이유만으로 좋은 설정이라고 판단할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young Generation에서 객체를 빠르게 할당하고 수집하는 과정은 Old Generation과 분리되어 있지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 계속 생성되면 Young Generation이 소모되고 Young GC가 발생합니다. Young GC에서 살아남은 객체는 Survivor Space로 복사되거나 Old Generation으로 승격됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할당 속도와 객체 생존율이 모두 높다면 Old Generation의 사용량도 빠르게 증가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Old Generation에 객체를 승격하거나 새로운 객체를 할당할 공간을 확보하기 어려워지면 Full GC가 수행될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;객체 생성
    &amp;darr;
Young Generation 사용량 증가
    &amp;darr;
Young GC
    &amp;darr;
생존 객체 복사 또는 승격
    &amp;darr;
Old Generation 사용량 증가
    &amp;darr;
Full GC 가능성 증가
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel GC는 이러한 수집 작업에 여러 CPU 코어를 활용하여 전체 처리량을 높이는 것을 목표로 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 높은 처리량이 짧은 중단 시간을 의미하는 것은 아닙니다. 특히 Full GC에서는 전체 힙과 많은 생존 객체를 처리해야 할 수 있으므로 긴 STW가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Parallel GC를 평가할 때는 단순히 수집 속도만 비교하지 않고 애플리케이션에서 중요한게 무엇인지 아래 질문을 함께 살펴봐야 합니다.&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;Old Generation의 증가 속도를 낮추는가&lt;/li&gt;
&lt;li&gt;Full GC 중단 시간을 줄여야 하는가&lt;/li&gt;
&lt;li&gt;전체 작업을 더 빠르게 완료해야 하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기준이 정해져야 처리량, 할당 속도, 생존율, 승격량, 중단 시간과 힙 크기를 어떤 관점에서 측정할지도 분명해집니다.&lt;/p&gt;</description>
      <category>Development/JAVA</category>
      <author>KoreaNirsa</author>
      <guid isPermaLink="true">https://nirsa.tistory.com/494</guid>
      <comments>https://nirsa.tistory.com/494#entry494comment</comments>
      <pubDate>Wed, 29 Jul 2026 18:17:33 +0900</pubDate>
    </item>
    <item>
      <title>[Java GC] Young Generation의 객체 할당과 수집: TLAB과 Survivor Space의 복사 수집</title>
      <link>https://nirsa.tistory.com/493</link>
      <description>&lt;blockquote data-pm-slice=&quot;1 1 []&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;Young Generation의 객체 할당과 수집: TLAB과 Survivor Space의 복사 수집&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;새로운 객체는 Young Generation 안에서 실제로 어떻게 만들어질까요? 여러 스레드가 동시에 객체를 생성하면 같은 메모리 공간을 두고 충돌하지는 않을까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한 Young GC가 발생했을 때 살아남은 객체는 어디로 이동하며, Survivor 영역을 두 개 사용하는 이유는 무엇일까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 질문을 이해하려면 다음 두 가지 구조를 함께 봐야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TLAB : 여러 스레드가 객체를 빠르게 할당하기 위한 구조&lt;/li&gt;
&lt;li&gt;Survivor Space의 복사 수집 : Young GC에서 살아남은 객체를 다른 Survivor Space나 Old Generation으로 이동시키는 방식&lt;/li&gt;
&lt;/ul&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;대부분의 새 객체는 Young Generation의 Eden 영역에 할당됩니다.&lt;/li&gt;
&lt;li&gt;TLAB은 Eden의 일부를 스레드별로 나누어 객체를 빠르게 할당할 수 있게 합니다.&lt;/li&gt;
&lt;li&gt;Young GC가 발생하면 Eden과 현재 사용 중인 Survivor에서 살아남은 객체를 비어 있는 Survivor로 복사합니다.&lt;/li&gt;
&lt;li&gt;두 Survivor 영역은 From과 To 역할을 번갈아 수행합니다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;여러 번 살아남거나 Survivor에 들어갈 수 없는 객체는&amp;nbsp;Old Generation으로 승격될 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Young Generation은 어떻게 구성될까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;전통적인 세대별 힙 구조에서 Young Generation은 다음과 같이 구성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtcybC/dJMcacxe0L8/8oWk8wOmqq3Smi1YmkMWj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtcybC/dJMcacxe0L8/8oWk8wOmqq3Smi1YmkMWj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtcybC/dJMcacxe0L8/8oWk8wOmqq3Smi1YmkMWj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtcybC%2FdJMcacxe0L8%2F8oWk8wOmqq3Smi1YmkMWj1%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;583&quot; height=&quot;173&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;각 영역의 역할은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;영역&lt;/td&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Eden&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;대부분의 새 객체가 처음 할당되는 영역입니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Survivor 0&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Young GC에서 살아남은 객체를 보관하는 영역입니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Survivor 1&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;다른 Survivor와 번갈아 생존 객체를 보관합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Survivor 0과 Survivor 1은 고정된 역할을 가지지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC가 반복될 때 한쪽은 생존 객체를 읽어오는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;From Survivor&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;다른 한쪽은 생존 객체를 복사해 넣는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;To Survivor&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 역할을 맡습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;현재 사용 중인 Survivor
&amp;rarr; From Survivor

현재 비어 있는 Survivor
&amp;rarr; To Survivor&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음 Young GC에서는 두 영역의 역할이 서로 바뀝니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;새 객체는 Eden에서 시작합니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일반적인 세대별 힙 구조에서는 대부분의 새 객체가 Eden 영역에 할당됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User user = new User();
Order order = new Order();
String message = new String(&quot;hello&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개념적으로는 다음과 같은 흐름입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2109&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOWfE9/dJMcaiKU4k1/DYXrfUNpqzOEpm0oCtkz60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOWfE9/dJMcaiKU4k1/DYXrfUNpqzOEpm0oCtkz60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOWfE9/dJMcaiKU4k1/DYXrfUNpqzOEpm0oCtkz60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOWfE9%2FdJMcaiKU4k1%2FDYXrfUNpqzOEpm0oCtkz60%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;554&quot; height=&quot;205&quot; data-origin-width=&quot;2109&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;애플리케이션이 실행되는 동안 객체 생성은 매우 자주 발생할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;웹 애플리케이션에서는 요청 하나를 처리하는 동안에도 다음과 같은 객체가 만들어질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;요청 DTO&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;응답 DTO&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;문자열&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;컬렉션&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;조회 결과 객체&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;임시 계산 객체&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;람다와 스트림 처리 과정에서 생성되는 객체&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;요청 수신
    &amp;darr;
DTO 생성
    &amp;darr;
서비스 객체 생성 및 가공
    &amp;darr;
응답 객체 생성
    &amp;darr;
요청 종료 후 다수 객체가 더 이상 필요하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;약한 세대 가설에 따르면 이렇게 생성되는 많은 객체는 비교적 빠르게 사용이 끝납니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 새 객체를 Eden에 모아 두면 Young GC를 통해 짧은 수명의 객체를 집중적으로 회수할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여러 스레드가 동시에 객체를 만들면 어떻게 될까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;힙의 Eden 영역은 여러 애플리케이션 스레드가 함께 사용하는 공간입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음과 같이 여러 요청 처리 스레드가 동시에 객체를 생성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1832&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbKlTo/dJMcafHypEW/IVbIMuPfTBle9sj6KF52Jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbKlTo/dJMcafHypEW/IVbIMuPfTBle9sj6KF52Jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbKlTo/dJMcafHypEW/IVbIMuPfTBle9sj6KF52Jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbKlTo%2FdJMcafHypEW%2FIVbIMuPfTBle9sj6KF52Jk%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;496&quot; height=&quot;211&quot; data-origin-width=&quot;1832&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체를 연속된 메모리 공간에 배치한다면 JVM은 다음 객체가 들어갈 위치를 나타내는 정보를 관리해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를 개념적으로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;할당 포인터&lt;/span&gt;&lt;/b&gt;&lt;span&gt;라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Eden 시작

[이미 사용한 공간][다음 할당 위치 &amp;rarr;][남은 공간]

Eden 끝&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체를 할당할 때는 다음과 같이 처리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재 할당 포인터 확인
    &amp;darr;
객체 크기만큼 공간 확보
    &amp;darr;
객체 저장
    &amp;darr;
할당 포인터를 앞으로 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이처럼 포인터를 앞으로 이동하며 객체를 배치하는 방식을 범프 포인터 할당(Bump Pointer Allocation)이라고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;문제는 여러 스레드가 동일한 할당 포인터를 동시에 변경하려 할 수 있다는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2472&quot; data-origin-height=&quot;1255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z2xHR/dJMcagTXgVl/G83iIANLttBrkj8O9wfGEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z2xHR/dJMcagTXgVl/G83iIANLttBrkj8O9wfGEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z2xHR/dJMcagTXgVl/G83iIANLttBrkj8O9wfGEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ2xHR%2FdJMcagTXgVl%2FG83iIANLttBrkj8O9wfGEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;514&quot; height=&quot;261&quot; data-origin-width=&quot;2472&quot; data-origin-height=&quot;1255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;모든 객체 할당마다 하나의 공유 포인터를 동기화해야 한다면 객체 생성 비용이 커질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 문제를 줄이기 위해 사용하는 구조가 TLAB입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;TLAB이란&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;TLAB(Thread-Local Allocation Buffer)은 각 애플리케이션 스레드에 할당되는 작은 객체 할당 영역입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;핫스팟 JVM은 Eden의 일부를 스레드별 TLAB으로 나누어 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2731&quot; data-origin-height=&quot;1255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bllh4d/dJMcafU4HCl/9JEREdY1v6W2yHjtlNp7b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bllh4d/dJMcafU4HCl/9JEREdY1v6W2yHjtlNp7b1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bllh4d/dJMcafU4HCl/9JEREdY1v6W2yHjtlNp7b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbllh4d%2FdJMcafU4HCl%2F9JEREdY1v6W2yHjtlNp7b1%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;634&quot; height=&quot;291&quot; data-origin-width=&quot;2731&quot; data-origin-height=&quot;1255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;각 스레드는 자신의 TLAB 안에서 독립적으로 객체를 할당합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스레드 A : 스레드 A의 TLAB에서 객체 할당&lt;/li&gt;
&lt;li&gt;스레드 B : 스레드 B의 TLAB에서 객체 할당&lt;/li&gt;
&lt;li&gt;스레드 C : 스레드 C의 TLAB에서 객체 할당&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다른 스레드의 TLAB을 직접 사용하지 않으므로, 일반적인 객체 할당 경로에서 공유 할당 포인터에 대한 경쟁을 줄일 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 TLAB은 별도의 힙이 아니라, Eden 영역 일부를 특정 스레드가 객체 할당에 사용하도록 나눈 공간입니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;TLAB에서는 객체를 어떻게 할당할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;각 TLAB은 자신만의 할당 시작 위치와 끝 위치를 가집니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;스레드 A의 TLAB

[객체 A][객체 B][할당 포인터 &amp;rarr;][남은 공간]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;스레드가 새로운 객체를 만들면 다음 과정을 거칩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5800&quot; data-origin-height=&quot;1215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crAfrr/dJMcaazsmyX/21IPAwGbGE91ZdfyOSkhb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crAfrr/dJMcaazsmyX/21IPAwGbGE91ZdfyOSkhb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crAfrr/dJMcaazsmyX/21IPAwGbGE91ZdfyOSkhb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrAfrr%2FdJMcaazsmyX%2F21IPAwGbGE91ZdfyOSkhb1%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;5800&quot; height=&quot;1215&quot; data-origin-width=&quot;5800&quot; data-origin-height=&quot;1215&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;TLAB 내부에서는 대체로 다음과 같은 단순한 연산으로 객체를 할당할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;새 할당 위치
= 현재 할당 위치 + 객체 크기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체를 놓을 공간이 TLAB 끝을 넘지 않는다면 현재 위치에 객체를 저장한 뒤 포인터만 앞으로 이동하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;할당 전

[객체 A][객체 B][포인터 &amp;rarr;][빈 공간]


새 객체 C 할당

[객체 A][객체 B][객체 C][포인터 &amp;rarr;][빈 공간]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 방식은 일반적인 메모리 할당처럼 빈 공간 목록을 탐색하거나 복잡한 자료구조를 확인하지 않아도 되므로 빠르게 처리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;TLAB이 객체 할당을 빠르게 만드는 이유&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;TLAB을 사용하면 각 스레드는 대부분의 객체를 자신의 영역에서 할당할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2635&quot; data-origin-height=&quot;1360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPIRmr/dJMcaasGViD/ERkz3I87INqLZ9sxCV0VKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPIRmr/dJMcaasGViD/ERkz3I87INqLZ9sxCV0VKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPIRmr/dJMcaasGViD/ERkz3I87INqLZ9sxCV0VKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPIRmr%2FdJMcaasGViD%2FERkz3I87INqLZ9sxCV0VKk%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;575&quot; height=&quot;297&quot; data-origin-width=&quot;2635&quot; data-origin-height=&quot;1360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;공유 영역에서 직접 할당&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;TLAB에서 할당&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;할당 위치&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;여러 스레드가 공유합니다.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;스레드마다 별도로 가집니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;동시 접근&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;충돌 가능성이 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;일반적인 할당에서는 충돌이 적습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;포인터 관리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;공유 포인터 갱신이 필요합니다.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;스레드별 포인터를 갱신합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;객체 할당 비용&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;동기화 비용이 발생할 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;범프 포인터 방식으로 빠르게 처리할 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;TLAB이 있더라도 Eden 전체를 스레드별로 고정 분할해 영구적으로 소유하는 것은 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;스레드의 객체 할당량에 따라 TLAB의 크기가 달라질 수 있고, 기존 TLAB을 다 사용하면 새로운 TLAB을 받을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;모든 객체가 TLAB에 할당될까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;TLAB은 일반적인 객체 할당의 주요 경로이지만, 모든 객체가 반드시 TLAB에 할당되는 것은 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음과 같은 경우에는 TLAB 밖에서 할당될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;현재 TLAB의 남은 공간보다 객체가 큰 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;새로운 TLAB을 받는 것보다 외부 할당이 효율적인 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;JVM의 TLAB 설정이나 상태에 따라 외부 할당이 선택되는 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;사용하는 가비지 컬렉터가 큰 객체를 별도로 처리하는 경우&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5201&quot; data-origin-height=&quot;1657&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJ4G3D/dJMcadJAnwB/5hYLN5s305gaAv9P3P0cEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJ4G3D/dJMcadJAnwB/5hYLN5s305gaAv9P3P0cEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJ4G3D/dJMcadJAnwB/5hYLN5s305gaAv9P3P0cEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ4G3D%2FdJMcadJAnwB%2F5hYLN5s305gaAv9P3P0cEk%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;5201&quot; height=&quot;1657&quot; data-origin-width=&quot;5201&quot; data-origin-height=&quot;1657&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 다음처럼 이해하는 것이 정확합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분의 일반적인 작은 객체 : TLAB에 빠르게 할당될 가능성 높음&lt;/li&gt;
&lt;li&gt;모든 객체 : 반드시 TLAB에 할당되는 것은 아님&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;TLAB은 객체의 생존 기간을 관리하는 공간이 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;TLAB에 생성된 객체도 실제로는 Eden에 존재&lt;/b&gt;하며, Young GC가 발생하면 다른 Eden 객체와 함께 생존 여부를 검사받습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;TLAB에 남은 공간은 어떻게 될까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;스레드가 새로운 TLAB을 받으려 할 때 기존 TLAB에 작은 공간이 남아 있을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;기존 TLAB

[객체 A][객체 B][사용하기 애매한 작은 공간]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;남은 공간이 새 객체를 저장하기에 부족하다면 해당 공간이 즉시 다른 스레드의 일반 객체 할당에 사용되지는 않을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이처럼 TLAB별로 작게 남은 공간은 일시적인 내부 단편화처럼 보일 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 Young GC가 발생하면 Eden 영역이 정리되고, 다음 객체 할당을 위한 공간으로 다시 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, TLAB은 할당 성능을 높이는 대신 일부 잔여 공간이 생길 수 있으며, JVM은 할당 패턴에 따라 TLAB 크기를 조절해 이러한 비용과 성능 사이의 균형을 맞춥니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Young GC는 언제 발생할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체가 계속 생성되면 Eden의 사용량이 증가합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;객체 생성
    &amp;darr;
Eden 내부의 TLAB 등에 할당
    &amp;darr;
Eden의 남은 공간 감소
    &amp;darr;
새 객체를 할당하기 어려워짐
    &amp;darr;
Young GC 필요&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일반적으로 Eden에 새 객체를 할당할 공간이 부족해지면 Young Generation을 대상으로 수집이 발생할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를 흔히 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Young GC&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 또는 전통적으로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Minor GC&lt;/span&gt;&lt;/b&gt;&lt;span&gt;라고 부릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5729&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK2mO3/dJMcaasGVtf/qszyHZDaGMASmkJDkmN6ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK2mO3/dJMcaasGVtf/qszyHZDaGMASmkJDkmN6ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK2mO3/dJMcaasGVtf/qszyHZDaGMASmkJDkmN6ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK2mO3%2FdJMcaasGVtf%2FqszyHZDaGMASmkJDkmN6ak%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;5729&quot; height=&quot;305&quot; data-origin-width=&quot;5729&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC는 단순히 Eden만 비우는 작업은 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일반적으로 Eden과 현재 사용중인 From Survivor에 있는 객체를 함께 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 영역에서 살아 있는 객체를 찾아 비어 있는 To Survivor로 복사하거나 Old Generation으로 승격합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Young GC가 시작되면 무엇을 확인할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC에서는 GC Root와 참조 관계를 바탕으로 Young Generation 객체의 생존 여부를 판단합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음과 같은 객체 관계가 있다고 가정하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;1255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/94Z79/dJMcafAJMOF/wxRYjqYekIcPjiUKeM3K7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/94Z79/dJMcafAJMOF/wxRYjqYekIcPjiUKeM3K7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/94Z79/dJMcafAJMOF/wxRYjqYekIcPjiUKeM3K7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F94Z79%2FdJMcafAJMOF%2FwxRYjqYekIcPjiUKeM3K7k%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;351&quot; height=&quot;257&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;1255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;GC Root에서 객체 A와 객체 B에는 도달할 수 있습니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체 C, D, E에 다른 유효한 참조 경로가 없다면 해당 객체들은 가비지 객체가 될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dns&quot;&gt;&lt;code&gt;객체 A
&amp;rarr; GC Root에서 직접 도달 가능
&amp;rarr; 생존

객체 B
&amp;rarr; 객체 A를 통해 도달 가능
&amp;rarr; 생존

객체 C, D, E
&amp;rarr; GC Root에서 도달 불가능
&amp;rarr; 회수 대상&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 글에서 설명하는 전통적인 Young Generation 모델에서는 살아남은 객체를 다른 영역으로 복사하거나 승격합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가비지 객체를 하나씩 지우기보다 살아 있는 객체를 목적지 공간으로 이동시킨 뒤 기존 Eden과 From Survivor를 다시 사용하는 방식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Survivor 영역을 두 개 사용하는 이유&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young Generation에는 일반적으로 두 개의 Survivor 영역이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Survivor 0
Survivor 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 영역은 GC가 발생할 때 From과 To 역할을 번갈아 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1531&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CxdCe/dJMcaalOlpN/tXtbQw5IgIXltDK4gecx6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CxdCe/dJMcaalOlpN/tXtbQw5IgIXltDK4gecx6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CxdCe/dJMcaalOlpN/tXtbQw5IgIXltDK4gecx6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCxdCe%2FdJMcaalOlpN%2FtXtbQw5IgIXltDK4gecx6K%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;518&quot; height=&quot;103&quot; data-origin-width=&quot;1531&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC가 발생하면 Eden과 From Survivor에서 살아남은 객체들을 To Survivor로 복사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Eden에서 살아남은 객체
+
From Survivor에서 다시 살아남은 객체&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;복사가 끝나면 기존 Eden과 From Survivor는 비워집니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# Young GC 전

Eden : 새 객체들이 존재
From Survivor : 이전 GC에서 살아남은 객체들이 존재
To Survivor : 비어 있음&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# Young GC 후

Eden : 비워짐
기존 From Survivor : 비워짐
기존 To Survivor : 생존 객체를 가진 새로운 From Survivor가 됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음 Young GC에서는 두 Survivor의 역할이 반대로 바뀝니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Survivor Space의 복사 수집이란&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC에서는 살아 있는 객체를 현재 위치에 그대로 둔 채 가비지만 하나씩 지우는 대신, 생존 객체를 다른 공간으로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;복사하거나 이동&lt;/span&gt;&lt;/b&gt;&lt;span&gt;하는 방식을 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;전통적인 Eden과 두 Survivor Space 모델에서는 Eden과 현재 사용 중인 From Survivor의 생존 객체를 비어 있는 To Survivor로 복사합니다. 객체의 나이, Survivor의 여유 공간, 수집기의 정책에 따라 일부 객체는 Old Generation으로 승격될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;6305&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brx9Bz/dJMcagNfrVL/ZXiHa1GijfmYlx0Q2qqnH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brx9Bz/dJMcagNfrVL/ZXiHa1GijfmYlx0Q2qqnH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brx9Bz/dJMcagNfrVL/ZXiHa1GijfmYlx0Q2qqnH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrx9Bz%2FdJMcagNfrVL%2FZXiHa1GijfmYlx0Q2qqnH0%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;6305&quot; height=&quot;872&quot; data-origin-width=&quot;6305&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 글에서는 이 과정을 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Survivor Space를 이용한 복사 수집&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이라고 표현합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;GC 알고리즘 이론에는 메모리를 From-space와 To-space로 나누는 세미스페이스 복사 수집기(Semispace Copying Collector)가 존재합니다. 그러나 HotSpot의 전통적인 Young Generation은 Eden과 두 Survivor Space로 구성되므로, Young Generation 전체를 단순한 세미스페이스 구조라고 부르는 것은 정확하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# 고전적인 세미스페이스 복사 수집

From-space
    &amp;darr; 생존 객체 복사
To-space&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# 전통적인 HotSpot Young Generation 모델

Eden + From Survivor
    &amp;darr; 생존 객체 복사 또는 승격
To Survivor 또는 Old Generation&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;핵심은 가비지 객체를 다른 위치로 옮기는 것이 아니라, &lt;/span&gt;&lt;b&gt;&lt;span&gt;살아 있는 객체만 목적지 공간으로 이동시킨 뒤 기존 Eden과 From Survivor를 다시 사용할 수 있게 만드는 것&lt;/span&gt;&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;첫 번째 Young GC에서는 어떻게 이동할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 Eden에 새로운 객체들이 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Eden

[객체 A][객체 B][객체 C][객체 D]

Survivor 0
[비어 있음]

Survivor 1
[비어 있음]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체 A와 객체 C만 살아 있다고 가정하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;첫 번째 Young GC가 발생하면 살아남은 객체를 한쪽 Survivor로 복사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n03gh/dJMcajwnPhY/2Ee8T2yOCXZKLOw5mmY5E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n03gh/dJMcajwnPhY/2Ee8T2yOCXZKLOw5mmY5E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n03gh/dJMcajwnPhY/2Ee8T2yOCXZKLOw5mmY5E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn03gh%2FdJMcajwnPhY%2F2Ee8T2yOCXZKLOw5mmY5E0%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;588&quot; height=&quot;192&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;GC 이후의 상태는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Eden
[비어 있음]

Survivor 0
[객체 A][객체 C]

Survivor 1
[비어 있음]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 시점에서 Survivor 0은 From Survivor 역할을 맡고, Survivor 1은 다음 GC에서 사용할 To Survivor가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;두 번째 Young GC에서는 어떻게 이동할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;첫 번째 GC 이후 Eden에는 다시 새로운 객체가 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Eden
[객체 E][객체 F][객체 G]

From Survivor
[객체 A][객체 C]

To Survivor
[비어 있음]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 번째 Young GC에서는 Eden과 From Survivor에 있는 객체를 함께 확인합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체 A, E, G가 살아 있다고 가정해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2961&quot; data-origin-height=&quot;990&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bayAU2/dJMcaiKU5yy/kuJugo3hC7eKI54kWDTPn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bayAU2/dJMcaiKU5yy/kuJugo3hC7eKI54kWDTPn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bayAU2/dJMcaiKU5yy/kuJugo3hC7eKI54kWDTPn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbayAU2%2FdJMcaiKU5yy%2FkuJugo3hC7eKI54kWDTPn0%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;615&quot; height=&quot;206&quot; data-origin-width=&quot;2961&quot; data-origin-height=&quot;990&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;GC 이후에는 기존 To Survivor가 새로운 From Survivor가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Eden
[비어 있음]

기존 From Survivor
[비어 있음]

기존 To Survivor
[객체 A][객체 E][객체 G]
&amp;rarr; 새로운 From Survivor&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Survivor 영역의 역할은 계속 바뀝니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC가 반복될 때 두 Survivor 영역은 번갈아 사용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5103&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daHNPu/dJMcadJAn8a/CD5n3FKKaMcJBA8pv6Ugsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daHNPu/dJMcadJAn8a/CD5n3FKKaMcJBA8pv6Ugsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daHNPu/dJMcadJAn8a/CD5n3FKKaMcJBA8pv6Ugsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaHNPu%2FdJMcadJAn8a%2FCD5n3FKKaMcJBA8pv6Ugsk%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;5103&quot; height=&quot;515&quot; data-origin-width=&quot;5103&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를 표로 정리하면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Young GC&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;From Survivor&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;To Survivor&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;첫 번째&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;없음&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Survivor 0&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;두 번째&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Survivor 0&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Survivor 1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;세 번째&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Survivor 1&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Survivor 0&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;네 번째&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Survivor 0&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Survivor 1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;첫 번째 Young GC에서는 기존 Survivor에 객체가 없을 수 있으므로 Eden의 생존 객체만 한쪽 Survivor로 복사됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이후부터는 Eden과 From Survivor의 생존 객체를 함께 To Survivor로 복사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;왜 한 개의 Survivor만 사용하지 않을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Survivor가 하나뿐이라면 현재 살아 있는 객체가 들어 있는 공간에 새로운 생존 객체를 다시 정리해서 배치해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 하나의 Survivor 안에서 기존 생존 객체 유지 + 새 생존 객체 추가 + 가비지 객체 제거 + 빈 공간 정리를 모두 수행하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이러한 경우 같은 공간 안에서 살아 있는 객체와 제거할 객체를 구분하고, 객체를 재배치하는 과정이 복잡해질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 두 Survivor를 사용하면 한쪽은 읽는 공간, 다른 한쪽은 쓰는 공간으로 명확하게 나눌 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1531&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/weod6/dJMcaa7j3vE/OXbXvAsrRBFYLZzrlBNW1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/weod6/dJMcaa7j3vE/OXbXvAsrRBFYLZzrlBNW1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/weod6/dJMcaa7j3vE/OXbXvAsrRBFYLZzrlBNW1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fweod6%2FdJMcaa7j3vE%2FOXbXvAsrRBFYLZzrlBNW1k%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;560&quot; height=&quot;150&quot; data-origin-width=&quot;1531&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;From Survivor&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;이전 GC에서 살아남은 객체가 저장된 공간입니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;To Survivor&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;이번 GC에서 살아남은 객체를 복사할 비어 있는 공간입니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;복사가 끝나면 기존 From Survivor 전체를 비울 수 있으므로, 살아 있는 객체들이 To Survivor에 연속적으로 모입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# 복사 전
From Survivor
[생존 객체][가비지 객체][생존 객체][빈 공간]


복사 후
# To Survivor
[생존 객체][생존 객체][연속된 빈 공간]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 방식은 생존 객체 복사와 메모리 압축을 함께 수행하는 효과를 가집니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Young GC에서 객체의 나이는 어떻게 바뀔까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Survivor로 이동한 객체는 Young GC에서 살아남은 횟수와 관련된 객체 나이(Age)를 가집니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Eden에서 새로 생성
&amp;rarr; Age 0

첫 번째 Young GC 생존
&amp;rarr; Age 1

두 번째 Young GC 생존
&amp;rarr; Age 2

세 번째 Young GC 생존
&amp;rarr; Age 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2846&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNLD47/dJMcadv1EGG/Iq323uhUSqgRl4F3sWnGJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNLD47/dJMcadv1EGG/Iq323uhUSqgRl4F3sWnGJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNLD47/dJMcadv1EGG/Iq323uhUSqgRl4F3sWnGJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNLD47%2FdJMcadv1EGG%2FIq323uhUSqgRl4F3sWnGJK%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;2846&quot; height=&quot;410&quot; data-origin-width=&quot;2846&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span&gt;객체 나이는 실제 시간이나 객체가 생성된 지 몇 초가 지났는지를 의미하지 않습니다.&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC에서 객체가 살아남은 횟수를 나타내는 개념에 가깝습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여러 번의 GC에서 살아남은 객체는 단기 객체가 아닐 가능성이 높으므로 Old Generation으로 이동할 후보가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;모든 생존 객체가 Survivor로 이동할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC에서 살아남은 객체가 항상 Survivor로 이동하는 것은 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;생존 객체는 다음 두 경로 중 하나로 이동할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3618&quot; data-origin-height=&quot;1066&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R3UmG/dJMcacD4XWS/IokkAfYacfxqk3d1ICDNM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R3UmG/dJMcacD4XWS/IokkAfYacfxqk3d1ICDNM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R3UmG/dJMcacD4XWS/IokkAfYacfxqk3d1ICDNM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR3UmG%2FdJMcacD4XWS%2FIokkAfYacfxqk3d1ICDNM0%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;3618&quot; height=&quot;1066&quot; data-origin-width=&quot;3618&quot; data-origin-height=&quot;1066&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체가 Old Generation으로 이동할 수 있는 대표적인 상황은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;객체 나이가 승격 기준에 도달한 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;To Survivor에 생존 객체를 모두 저장할 공간이 부족한 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;JVM이 객체 나이 분포를 바탕으로 조기 승격을 결정한 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;수집기나 객체 크기에 따라 별도 승격 경로가 적용되는 경우&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 살아있는 객체를 Survivor에 보관할 수 있다면 To Survivor로 복사하고, Survivor의 공간이 부족하거나 승격 조건을 충족했다면 Old Generation으로 이동됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 Young GC의 결과는 단순히 생존 또는 제거 두 가지로만 나뉘지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 48.1395%;&quot;&gt;&lt;span&gt;객체 상태&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 51.7442%;&quot;&gt;&lt;span&gt;Young GC 결과&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 48.1395%;&quot;&gt;&lt;span&gt;GC Root에서 도달할 수 없음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 51.7442%;&quot;&gt;&lt;span&gt;메모리가 회수됩니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 48.1395%;&quot;&gt;&lt;span&gt;살아 있고 아직 승격 조건을 충족하지 않음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 51.7442%;&quot;&gt;&lt;span&gt;To Survivor로 복사됩니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 48.1395%;&quot;&gt;&lt;span&gt;살아 있고 승격 조건을 충족함&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 51.7442%;&quot;&gt;&lt;span&gt;Old Generation으로 승격됩니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; Young GC의 전체 처리 순서&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC의 전체 흐름을 정리하면 다음과 같습니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2723&quot; data-origin-height=&quot;6557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/psGU8/dJMcajptque/BEWdt8tXnQTR3ZA8ipKJ8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/psGU8/dJMcajptque/BEWdt8tXnQTR3ZA8ipKJ8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/psGU8/dJMcajptque/BEWdt8tXnQTR3ZA8ipKJ8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpsGU8%2FdJMcajptque%2FBEWdt8tXnQTR3ZA8ipKJ8K%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;460&quot; height=&quot;1108&quot; data-origin-width=&quot;2723&quot; data-origin-height=&quot;6557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;각 단계의 역할은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;단계&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;처리 내용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;객체 할당&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;대부분의 새 객체를 Eden 또는 TLAB에 할당합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Young GC 발생&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Eden에 객체를 할당할 공간이 부족해지면 수집이 발생할 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;생존 객체 탐색&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Eden과 From Survivor의 객체 중 도달 가능한 객체를 찾습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;객체 복사&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;아직 젊은 생존 객체를 To Survivor로 복사합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;객체 승격&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;승격 조건을 만족한 객체를 Old Generation으로 이동시킵니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;영역 초기화&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Eden과 기존 From Survivor를 비웁니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;역할 교체&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;기존 To Survivor가 새로운 From Survivor가 됩니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;TLAB과 Young GC는 어떻게 연결될까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;TLAB은 객체를 생성하는 단계에서 사용되고, Young GC는 생성된 객체의 생존 여부를 판단하는 단계에서 사용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;6220&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsOYs6/dJMcaiYqeT4/BNYzEDJVcaZI2Y20Gea971/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsOYs6/dJMcaiYqeT4/BNYzEDJVcaZI2Y20Gea971/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsOYs6/dJMcaiYqeT4/BNYzEDJVcaZI2Y20Gea971/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsOYs6%2FdJMcaiYqeT4%2FBNYzEDJVcaZI2Y20Gea971%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;6220&quot; height=&quot;410&quot; data-origin-width=&quot;6220&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 구조의 역할은 명확하게 구분됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;구조&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;사용 시점&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;목적&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;TLAB&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;객체 생성 시점&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;여러 스레드가 객체를 빠르게 할당하도록 돕습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Eden&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;객체 생성 이후&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;새 객체를 모아 두는 Young Generation의 영역입니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Survivor&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Young GC 이후&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;살아남은 객체를 보관합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Survivor 복사 수집&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Young GC 수행 중&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;생존 객체를 To Survivor나 Old로 이동시킵니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Old Generation&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;장수 객체 관리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;여러 번 살아남은 객체를 별도로 관리합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Young GC는 왜 효율적일 수 있을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;약한 세대 가설에 따르면 Young Generation에는 빠르게 사라지는 객체가 많이 존재할 가능성이 높습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC는 가비지 객체를 하나씩 이동시키지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;살아 있는 객체만 다른 공간으로 복사하고, 기존 Eden과 From Survivor 전체를 비웁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4323&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmijgm/dJMcahZy4bQ/mNPZ67YqSnhnTSjhpEV54k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmijgm/dJMcahZy4bQ/mNPZ67YqSnhnTSjhpEV54k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmijgm/dJMcahZy4bQ/mNPZ67YqSnhnTSjhpEV54k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcmijgm%2FdJMcahZy4bQ%2FmNPZ67YqSnhnTSjhpEV54k%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;4323&quot; height=&quot;410&quot; data-origin-width=&quot;4323&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 방식은 생존 객체의 수가 적으면 적은 수의 객체만 복사하고 기존 영역 전체를 빠르게 비우므로 생존 객체의 수가 적을수록 유리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반대로 Young Generation에서 많은 객체가 계속 살아남는다면 복사해야 할 객체가 증가하고 Survivor와 Old Generation의 부담도 커질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Young GC가 발생하면 애플리케이션은 멈출까&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC의 중단 방식은 사용하는 가비지 컬렉터에 따라 다릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Serial GC, Parallel GC, G1 GC의 Young Collection처럼 생존 객체의 이동과 참조 갱신을 정지 구간에서 수행하는 수집기에서는 애플리케이션 스레드가 일시적으로 멈추는 &lt;b&gt;STW(Stop-The-World)&lt;/b&gt;가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4659&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Moz1Z/dJMcaf8tWzV/r1rYEUo3dsqPohucQcFnh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Moz1Z/dJMcaf8tWzV/r1rYEUo3dsqPohucQcFnh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Moz1Z/dJMcaf8tWzV/r1rYEUo3dsqPohucQcFnh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMoz1Z%2FdJMcaf8tWzV%2Fr1rYEUo3dsqPohucQcFnh0%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;4659&quot; height=&quot;305&quot; data-origin-width=&quot;4659&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다만 모든 세대별 수집기의 Young GC가 같은 방식으로 동작하는 것은 아닙니다. 세대별 ZGC나 세대별 Shenandoah처럼 주요 작업을 애플리케이션과 동시에 수행하도록 설계된 수집기도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 다음처럼 이해하는 것이 정확합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전통적인 HotSpot 수집기와 G1의 Young Collection : STW 구간에서 Young 영역을 수집&lt;/li&gt;
&lt;li&gt;동시성을 강조한 세대별 수집기 : 주요 수집 작업의 상당 부분을 애플리케이션과 동시에 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;STW 기반 Young GC의 실제 중단 시간은 다음 조건에 따라 달라질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Young Generation 크기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;객체 할당 속도&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;수집 시점의 생존 객체 수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Survivor 공간 크기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;승격되는 객체의 수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;사용하는 가비지 컬렉터&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;CPU와 GC 작업 스레드 수&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young GC라는 이름만으로 항상 짧다고 단정할 수는 없습니다. 특히 생존 객체나 승격 대상이 많으면 이동하고 갱신해야 하는 작업량도 증가할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Young GC를 이해할 때 주의할 점&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Eden, 두 Survivor 영역, From과 To를 사용하는 구조는 세대별 가비지 컬렉션을 이해하기 위한 대표적인 모델입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 모든 가비지 컬렉터가 힙을 완전히 동일한 방식으로 구현하는 것은 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;수집기에 따라 다음 요소가 달라질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Young Generation의 물리적 배치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Eden과 Survivor의 크기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;객체가 이동하는 단위&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;큰 객체의 할당 위치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;승격 기준&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Young GC의 병렬 처리 방식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;리전 단위의 세대 관리 여부&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 리전 기반 수집기는 힙을 여러 개의 작은 리전으로 나누고, 각 리전에 Eden이나 Survivor 역할을 부여할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 다음과 같이 이해하는 것이 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Eden + Survivor 0 + Survivor 1 : 전통적인 Young Generation을 설명하는 대표적인 구조&lt;/li&gt;
&lt;li&gt;TLAB : 핫스팟에서 일반 객체 할당을 빠르게 만드는 주요 구조&lt;/li&gt;
&lt;li&gt;From / To Survivor : 생존 객체를 복사하는 기본 원리&lt;/li&gt;
&lt;li&gt;구체적인 물리적 배치 : 사용하는 가비지 컬렉션에 따라 달라질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핵심 정리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Young Generation은 일반적으로 Eden과 두 개의 Survivor 영역으로 구성됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;대부분의 새 객체는 Eden에서 시작합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;TLAB은 Eden의 일부를 스레드별 객체 할당 영역으로 사용하는 구조입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;각 스레드는 자신의 TLAB에서 범프 포인터 방식으로 객체를 빠르게 할당할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;모든 객체가 반드시 TLAB에 할당되는 것은 아닙니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Eden에 객체를 할당할 공간이 부족해지면 Young GC가 발생할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Young GC는 Eden과 From Survivor의 객체를 대상으로 생존 여부를 확인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;살아남은 객체는 To Survivor로 복사되거나 Old Generation으로 승격됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;GC Root에서 도달할 수 없는 객체가 차지한 공간은 회수됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;두 Survivor 영역은 From과 To 역할을 번갈아 수행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;전통적인 Young Generation 모델에서는 생존 객체를 To Survivor나 Old Generation으로 이동시키고, 기존 Eden과 From Survivor를 다시 사용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;객체가 Young GC에서 살아남으면 객체 나이가 증가할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;여러 번 살아남거나 Survivor 공간이 부족한 경우 객체가 Old Generation으로 승격될 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Young GC 중 객체 복사와 참조 갱신을 위해 STW가 발생할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;구체적인 Young Generation 구현은 사용하는 가비지 컬렉터에 따라 달라질 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Young Generation에서는 객체를 빠르게 만들고, 짧게 살아남는 객체를 효율적으로 제거하기 위한 구조를 함께 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체 생성 단계에서는 TLAB을 사용해 여러 스레드가 각자의 공간에서 빠르게 객체를 할당합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2311&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOLc62/dJMcaaMYiUE/WF2uvbqFaTb2dyH4XmdwX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOLc62/dJMcaaMYiUE/WF2uvbqFaTb2dyH4XmdwX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOLc62/dJMcaaMYiUE/WF2uvbqFaTb2dyH4XmdwX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOLc62%2FdJMcaaMYiUE%2FWF2uvbqFaTb2dyH4XmdwX0%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;2311&quot; height=&quot;305&quot; data-origin-width=&quot;2311&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Eden에 객체를 할당할 공간이 부족해지면 Young GC가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가비지 컬렉터는 Eden과 From Survivor에서 살아남은 객체를 찾아 To Survivor로 복사하거나 Old Generation으로 승격합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;6421&quot; data-origin-height=&quot;1283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu3hjn/dJMcaaGf8vh/LNz1fA6MksVGplcXGeyxBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu3hjn/dJMcaaGf8vh/LNz1fA6MksVGplcXGeyxBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu3hjn/dJMcaaGf8vh/LNz1fA6MksVGplcXGeyxBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu3hjn%2FdJMcaaGf8vh%2FLNz1fA6MksVGplcXGeyxBK%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;6421&quot; height=&quot;1283&quot; data-origin-width=&quot;6421&quot; data-origin-height=&quot;1283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 흐름의 핵심은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TLAB : 새 객체를 빠르게 할당&lt;/li&gt;
&lt;li&gt;Eden : 대부분의 새 객체가 시작하는 영역&lt;/li&gt;
&lt;li&gt;Young GC : 생존 객체와 가비지 객체를 구분&lt;/li&gt;
&lt;li&gt;From / To Survivor : 생존 객체를 복사하며 번갈아가며 사용&lt;/li&gt;
&lt;li&gt;Promotion : 오래 살아남은 객체를 Old Generation으로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체 할당과 생존 객체 복사가 연결되면서 Young Generation은 짧은 수명의 객체를 빠르게 처리하고, 오래 살아남는 객체만 다음 세대로 보내는 구조를 완성합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Development/JAVA</category>
      <author>KoreaNirsa</author>
      <guid isPermaLink="true">https://nirsa.tistory.com/493</guid>
      <comments>https://nirsa.tistory.com/493#entry493comment</comments>
      <pubDate>Wed, 29 Jul 2026 11:36:59 +0900</pubDate>
    </item>
    <item>
      <title>[Java GC] 객체 수명과 약한 세대 가설: 힙을 세대로 나누는 이유</title>
      <link>https://nirsa.tistory.com/492</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체 수명과 약한 세대 가설: 힙을 세대로 나누는 이유&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바의 가비지 컬렉션을 공부하다 보면 다음과 같은 용어가 반복해서 등장합니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Eden 영역
Survivor 영역
Old Generation
Minor GC
객체 승격
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙을 여러 영역으로 나누는 이유는 객체마다 살아남는 시간이 다르기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 객체는 생성된 직후 잠깐 사용되고 사라집니다. 반면 애플리케이션이 실행되는 동안 계속 참조되면서 오랫동안 살아남는 객체도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public void process() {
    String message = &quot;임시 메시지&quot;;
    User user = userRepository.findById(1L);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 message처럼 메서드 내부에서 잠깐 사용하는 객체는 비교적 빠르게 필요 없어질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 캐시, 설정 정보, 장시간 유지되는 세션 데이터처럼 여러 요청에서 계속 사용하는 객체는 오랫동안 살아남을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 객체 수명의 차이를 가비지 컬렉션에 활용하는 대표적인 생각이 &lt;b&gt;약한 세대 가설(Weak Generational Hypothesis)&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;객체가 얼마나 오래 살아남을지는 생성 시점에 정확히 알기 어렵습니다.&lt;/li&gt;
&lt;li&gt;일반적으로 많은 객체는 생성된 뒤 비교적 빠르게 더 이상 사용되지 않는 경향이 있습니다.&lt;/li&gt;
&lt;li&gt;새 객체와 오래 살아남은 객체를 서로 다른 영역에서 관리하면 가비지 컬렉션의 처리 범위와 비용을 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;전통적인 세대별 힙 구조에서는 새 객체가 Eden에서 시작하고, 살아남은 객체가 Survivor를 거쳐 Old Generation으로 이동합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;먼저 알아둘 가비지 컬렉션 용어&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세대별 가비지 컬렉션을 이해하려면 객체를 저장하고 회수하는 과정에서 자주 사용하는 몇 가지 용어를 먼저 정리해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 206px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;용어&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;할당&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;새 객체가 사용할 메모리 공간을 확보하는 작업입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;회수&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;더 이상 사용할 수 없는 객체가 차지한 메모리를 재사용할 수 있게 만드는 작업입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;병렬 처리&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;여러 GC 스레드가 동시에 수집 작업을 수행하는 방식입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;동시 처리&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;애플리케이션 스레드와 GC 스레드가 일부 작업을 함께 진행하는 방식입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;정확한 GC&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;어떤 값이 객체 참조인지 JVM이 정확하게 구분할 수 있는 방식입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;보수적 GC&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;메모리의 값이 참조인지 확실하지 않으면 참조일 가능성이 있다고 판단하는 방식입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;객체 이동&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;살아 있는 객체를 다른 메모리 위치로 옮기는 작업입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;압축&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;살아 있는 객체를 모아 연속된 빈 공간을 확보하는 작업입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;승격&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;젊은 세대에서 오래 살아남은 객체를 오래된 세대로 이동시키는 작업입니다.&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3202&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHaPrq/dJMb99NVYR1/bFNcBXqCl85wb9Jficp070/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHaPrq/dJMb99NVYR1/bFNcBXqCl85wb9Jficp070/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHaPrq/dJMb99NVYR1/bFNcBXqCl85wb9Jficp070/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHaPrq%2FdJMb99NVYR1%2FbFNcBXqCl85wb9Jficp070%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;161&quot; data-origin-width=&quot;3202&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 생성할 때는 메모리를 &lt;b&gt;할당&lt;/b&gt;합니다. 가비지 컬렉션이 더 이상 사용할 수 없는 객체를 처리할 때는 객체가 차지한 메모리를 &lt;b&gt;회수&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;할당
&amp;rarr; 새 객체가 사용할 공간을 확보합니다.

회수
&amp;rarr; 사용이 끝난 객체의 공간을 다시 사용할 수 있게 만듭니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Stop-The-World란&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉션을 설명할 때 자주 등장하는 개념이 &lt;b&gt;Stop-The-World, STW&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STW는 가비지 컬렉션 작업을 수행하기 위해 JVM이 애플리케이션 스레드의 실행을 일시적으로 멈추는 상태를 말합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4926&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cicDo4/dJMcaf8r0Io/0WykvyDBKKMFaks3J4gbC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cicDo4/dJMcaf8r0Io/0WykvyDBKKMFaks3J4gbC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cicDo4/dJMcaf8r0Io/0WykvyDBKKMFaks3J4gbC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcicDo4%2FdJMcaf8r0Io%2F0WykvyDBKKMFaks3J4gbC1%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;4926&quot; height=&quot;305&quot; data-origin-width=&quot;4926&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC가 객체의 참조 관계를 조사하는 동안 애플리케이션이 객체를 계속 생성하거나 참조 관계를 변경하면, 수집기가 확인하는 객체 그래프가 계속 달라질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 일부 GC 단계에서는 애플리케이션 스레드를 멈추고 객체 상태를 안정적으로 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 모든 가비지 컬렉터가 전체 수집 작업을 STW 상태에서만 진행하는 것은 아닙니다. 수집기에 따라 일부 작업은 애플리케이션과 동시에 수행하고, 반드시 필요한 특정 단계에서만 짧은 STW를 발생시키기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STW가 발생하면 해당 시간 동안 애플리케이션은 사용자 요청을 처리하지 못할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;STW 시간 증가
    &amp;darr;
요청 처리 지연
    &amp;darr;
응답 시간 증가
    &amp;darr;
사용자가 멈춤 현상을 느낄 수 있음
&lt;/code&gt;&lt;/pre&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기준&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;처리량&lt;/td&gt;
&lt;td&gt;전체 실행 시간 중 애플리케이션이 실제 작업을 수행한 비율&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일시 정지 시간&lt;/td&gt;
&lt;td&gt;한 번의 STW가 지속되는 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC 발생 빈도&lt;/td&gt;
&lt;td&gt;가비지 컬렉션이 얼마나 자주 발생하는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리 사용량&lt;/td&gt;
&lt;td&gt;가비지 컬렉터가 동작하는 데 필요한 추가 메모리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 이동과 압축은 왜 같이 등장할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉션은 가비지 객체의 공간만 비워 두는 방식으로 동작할 수도 있고, 살아남은 객체를 다른 위치로 옮길 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 이동하면 해당 객체를 가리키던 참조도 새로운 위치를 가리키도록 갱신해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3707&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bucttA/dJMcaixrRwh/iAKinxCkmGXiqysadE973K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bucttA/dJMcaixrRwh/iAKinxCkmGXiqysadE973K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bucttA/dJMcaixrRwh/iAKinxCkmGXiqysadE973K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbucttA%2FdJMcaixrRwh%2FiAKinxCkmGXiqysadE973K%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;3707&quot; height=&quot;532&quot; data-origin-width=&quot;3707&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축은 살아 있는 객체를 한쪽으로 모아 빈 공간을 연속되게 만드는 처리입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;압축 전&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;[객체 A][빈 공간][객체 B][빈 공간][객체 C]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;압축 후&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;[객체 A][객체 B][객체 C][연속된 빈 공간]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3638&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uYqw4/dJMcacquuyu/TqRCjw1bflPTpcvK0yN8o1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uYqw4/dJMcacquuyu/TqRCjw1bflPTpcvK0yN8o1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uYqw4/dJMcacquuyu/TqRCjw1bflPTpcvK0yN8o1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuYqw4%2FdJMcacquuyu%2FTqRCjw1bflPTpcvK0yN8o1%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;3638&quot; height=&quot;515&quot; data-origin-width=&quot;3638&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 이동과 압축은 메모리 단편화를 줄이고 새로운 객체를 할당하기 쉬운 상태를 만드는 데 도움이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 객체를 이동하고 참조를 갱신하는 작업에는 추가 비용이 발생합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체는 얼마나 오래 살아남을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체는 생성된 뒤 서로 다른 수명을 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 메서드 내부에서 잠깐 사용하는 객체가 있을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public String createMessage(String name) {
    String prefix = &quot;안녕하세요, &quot;;
    String message = prefix + name;

    return message;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prefix와 문자열 결합 과정에서 생성된 일부 객체는 메서드 실행이 끝난 뒤 빠르게 접근할 수 없는 상태가 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 다음과 같이 애플리케이션 실행 중 계속 유지되는 객체도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public class ApplicationConfig {

    private static final ApplicationConfig INSTANCE =
            new ApplicationConfig();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 필드에서 계속 참조되는 ApplicationConfig 객체는 장시간 살아남을 가능성이 큽니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3686&quot; data-origin-height=&quot;1255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVeY4a/dJMcaftUjws/dv0JmK1QZlzJvtSqliQYgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVeY4a/dJMcaftUjws/dv0JmK1QZlzJvtSqliQYgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVeY4a/dJMcaftUjws/dv0JmK1QZlzJvtSqliQYgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVeY4a%2FdJMcaftUjws%2Fdv0JmK1QZlzJvtSqliQYgk%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;845&quot; height=&quot;288&quot; data-origin-width=&quot;3686&quot; data-origin-height=&quot;1255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 JVM이 객체를 생성하는 순간에는 해당 객체가 얼마나 오래 살아남을지 정확히 알기 어렵다는 점입니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;새 객체 생성
    &amp;darr;
짧게 살지 오래 살지 아직 알 수 없음
    &amp;darr;
GC를 거치며 실제 생존 여부 확인
    &amp;darr;
계속 살아남으면 장수 객체로 판단
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 객체가 실제로 몇 번의 가비지 컬렉션을 통과했는지를 바탕으로 관리 영역을 구분하는 방식이 사용됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;약한 세대 가설이란&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;약한 세대 가설(Weak Generational Hypothesis)&lt;/b&gt;은 객체의 생존 특성에 관한 경험적 관찰입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 다음과 같은 두 가지 경향을 중심으로 설명합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;대부분의 객체는 생성된 뒤 비교적 빠르게 사용이 끝납니다.&lt;/li&gt;
&lt;li&gt;일정 시간 동안 살아남은 객체는 이후에도 계속 살아남을 가능성이 상대적으로 높습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 경향은 &lt;b&gt;대부분의 객체가 젊을 때 죽는다&lt;/b&gt;는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 경향은 여러 번의 가비지 컬렉션을 통과한 객체가 앞으로도 계속 사용될 가능성이 높다는 의미입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3526&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7JKYZ/dJMcah6p0d1/ALJ1wLyeGoi63p0DdkQ2w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7JKYZ/dJMcah6p0d1/ALJ1wLyeGoi63p0DdkQ2w1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7JKYZ/dJMcah6p0d1/ALJ1wLyeGoi63p0DdkQ2w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7JKYZ%2FdJMcah6p0d1%2FALJ1wLyeGoi63p0DdkQ2w1%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;3526&quot; height=&quot;780&quot; data-origin-width=&quot;3526&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 모든 객체가 반드시 짧게 산다는 의미가 아니라는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약한 세대 가설은 객체의 수명 분포에서 &lt;b&gt;짧게 살아남는 객체가 많이 나타나는 경향&lt;/b&gt;이 있다는 설명입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;왜 &amp;lsquo;약한&amp;rsquo; 세대 가설일까&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 프로그램과 모든 객체에 절대적으로 적용되는 법칙이 아니기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 구조와 사용 방식에 따라 오래 살아남는 객체가 많이 생성될 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 객체는 비교적 오래 살아남을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 설정 객체&lt;/li&gt;
&lt;li&gt;캐시에 저장된 객체&lt;/li&gt;
&lt;li&gt;장시간 유지되는 세션 객체&lt;/li&gt;
&lt;li&gt;스레드 풀과 커넥션 풀&lt;/li&gt;
&lt;li&gt;애플리케이션 전역에서 사용하는 싱글턴 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 약한 세대 가설은 절대적인 규칙이라기보다, 여러 프로그램에서 관찰되는 객체 생존 경향을 가비지 컬렉션 설계에 활용한 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;힙을 세대로 나누는 이유&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 객체를 하나의 영역에서 동일한 방식으로 처리한다고 가정해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉션이 발생할 때마다 힙 전체를 탐색해야 한다면, 짧게 사용되는 객체 몇 개를 회수하기 위해 오래 살아남은 객체까지 반복해서 확인해야 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3955&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v75mA/dJMcabE05ma/kKkKzhx5M8W7p8nyqfe0lK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v75mA/dJMcabE05ma/kKkKzhx5M8W7p8nyqfe0lK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v75mA/dJMcabE05ma/kKkKzhx5M8W7p8nyqfe0lK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv75mA%2FdJMcabE05ma%2FkKkKzhx5M8W7p8nyqfe0lK%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;3955&quot; height=&quot;305&quot; data-origin-width=&quot;3955&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 새로 생성된 객체가 모여 있는 영역과 오래 살아남은 객체가 모여 있는 영역을 나누면 상황이 달라집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1999&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XG9Bb/dJMcahywamG/4uoGCyoOAeVk1w2XKMW7qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XG9Bb/dJMcahywamG/4uoGCyoOAeVk1w2XKMW7qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XG9Bb/dJMcahywamG/4uoGCyoOAeVk1w2XKMW7qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXG9Bb%2FdJMcahywamG%2F4uoGCyoOAeVk1w2XKMW7qk%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;463&quot; height=&quot;181&quot; data-origin-width=&quot;1999&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세대를 나누면 가비지 컬렉터는 짧은 수명의 객체가 집중되는 영역을 우선적으로 수집할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 전체를 매번 검사하지 않고, 가비지가 많이 발생할 가능성이 높은 영역을 중심으로 처리할 수 있는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;새 객체가 많이 생성되는 영역
&amp;rarr; 가비지가 많이 발생할 가능성이 높음
&amp;rarr; 비교적 자주 수집

오래 살아남은 객체가 모인 영역
&amp;rarr; 가비지가 될 가능성이 상대적으로 낮음
&amp;rarr; 젊은 세대와 다른 주기로 수집
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;전통적인 세대별 힙 구조&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 세대별 가비지 컬렉션에서는 힙을 크게 다음과 같이 구분합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Young Generation
    ├─ Eden
    ├─ Survivor 0
    └─ Survivor 1

Old Generation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3115&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CG1gz/dJMcagNdl8n/SECE80UKOvR728AstQF9d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CG1gz/dJMcagNdl8n/SECE80UKOvR728AstQF9d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CG1gz/dJMcagNdl8n/SECE80UKOvR728AstQF9d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCG1gz%2FdJMcagNdl8n%2FSECE80UKOvR728AstQF9d0%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;3115&quot; height=&quot;655&quot; data-origin-width=&quot;3115&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 흐름도는 객체의 개념적인 이동 방향을 단순화한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Survivor 영역은 일반적으로 두 공간 중 하나를 비워 둔 상태에서, GC가 발생할 때 살아남은 객체를 다른 Survivor 영역으로 복사하는 방식으로 사용됩니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;세대 또는 영역&lt;/td&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eden&lt;/td&gt;
&lt;td&gt;대부분의 새 객체가 처음 할당되는 영역입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survivor&lt;/td&gt;
&lt;td&gt;Young GC에서 살아남은 객체가 이동하는 영역입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old Generation&lt;/td&gt;
&lt;td&gt;여러 번의 GC에서 살아남은 객체가 이동하는 영역입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;새 객체는 Eden에서 시작합니다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 세대별 힙 구조에서는 새 객체가 먼저 Eden 영역에 할당됩니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User user = new User();
Order order = new Order();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로 다음과 같이 객체가 Eden에 쌓입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2109&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SMGFM/dJMcah6p0fM/iDADIRGCUidrebHXWeR8qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SMGFM/dJMcah6p0fM/iDADIRGCUidrebHXWeR8qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SMGFM/dJMcah6p0fM/iDADIRGCUidrebHXWeR8qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSMGFM%2FdJMcah6p0fM%2FiDADIRGCUidrebHXWeR8qk%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;525&quot; height=&quot;194&quot; data-origin-width=&quot;2109&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eden 영역은 새로운 객체가 지속적으로 생성되는 공간입니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;객체 생성
    &amp;darr;
Eden에 할당
    &amp;darr;
Eden 사용량 증가
    &amp;darr;
Eden에 공간 부족
    &amp;darr;
Young Generation을 대상으로 GC 발생
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 새 객체가 빠르게 가비지 객체가 된다는 가정이 맞는다면, Eden을 수집할 때 많은 객체를 한 번에 제거할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;살아남은 객체는 Survivor로 이동합니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young Generation을 대상으로 가비지 컬렉션이 발생하면, Eden에 있는 객체 중 GC Root에서 도달할 수 있는 객체가 살아남습니다. 살아남은 객체는 Survivor 영역으로 이동할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3127&quot; data-origin-height=&quot;885&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8Z7hB/dJMcahSJjDr/As7mAIcdoZkCwcbFfjrSa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8Z7hB/dJMcahSJjDr/As7mAIcdoZkCwcbFfjrSa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8Z7hB/dJMcahSJjDr/As7mAIcdoZkCwcbFfjrSa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8Z7hB%2FdJMcahSJjDr%2FAs7mAIcdoZkCwcbFfjrSa1%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;3127&quot; height=&quot;885&quot; data-origin-width=&quot;3127&quot; data-origin-height=&quot;885&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 가비지 객체는 제거되고, 살아남은 객체만 다음 영역으로 복사됩니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Eden의 객체
    &amp;darr;
Young GC 실행
    &amp;darr;
살아 있는 객체 확인
    &amp;darr;
생존 객체는 Survivor로 이동
    &amp;darr;
가비지 객체의 공간은 회수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Survivor 영역은 왜 두 개일까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young Generation에는 일반적으로 두 개의 Survivor 영역이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 다음과 같이 표현합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Survivor 0
Survivor 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 현재 역할에 따라 다음과 같이 부릅니다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;From Survivor
To Survivor
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young GC가 발생하면 Eden과 현재 사용 중인 Survivor 영역의 살아 있는 객체를 비어 있는 Survivor 영역으로 복사합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3305&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YmZW6/dJMcahZw7fb/AXGIw8RJ7ep2V5ttepcSek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YmZW6/dJMcahZw7fb/AXGIw8RJ7ep2V5ttepcSek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YmZW6/dJMcahZw7fb/AXGIw8RJ7ep2V5ttepcSek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYmZW6%2FdJMcahZw7fb%2FAXGIw8RJ7ep2V5ttepcSek%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;3305&quot; height=&quot;780&quot; data-origin-width=&quot;3305&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 첫 번째 GC에서 Survivor 0으로 객체를 복사했다면, 다음 GC에서는 Survivor 0의 생존 객체를 Survivor 1로 이동할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3693&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vq2Q5/dJMcai5aJzK/vZep0TmBeK3eNhhgwguWw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vq2Q5/dJMcai5aJzK/vZep0TmBeK3eNhhgwguWw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vq2Q5/dJMcai5aJzK/vZep0TmBeK3eNhhgwguWw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVq2Q5%2FdJMcai5aJzK%2FvZep0TmBeK3eNhhgwguWw0%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;3693&quot; height=&quot;515&quot; data-origin-width=&quot;3693&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 Survivor 영역을 번갈아 사용하면 살아 있는 객체만 모아서 복사할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 빈 공간이 자연스럽게 연속적으로 확보되므로, 젊은 세대에서는 별도의 복잡한 압축 작업을 줄일 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체의 나이는 무엇일까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Survivor 영역에서 살아남은 객체에는 &lt;b&gt;객체 나이(Age)&lt;/b&gt;라는 개념이 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 나이는 실제 시간이나 초 단위를 의미하지 않으며 객체가 Young GC에서 살아남은 횟수와 관련된 값입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;객체 생성
&amp;rarr; 나이 0

첫 번째 Young GC 생존
&amp;rarr; 나이 증가

두 번째 Young GC 생존
&amp;rarr; 나이 다시 증가

계속 생존
&amp;rarr; 승격 후보
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3472&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBPdXQ/dJMcabLVicV/4VEepF0z8d02xvN9nbg3l0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBPdXQ/dJMcabLVicV/4VEepF0z8d02xvN9nbg3l0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBPdXQ/dJMcabLVicV/4VEepF0z8d02xvN9nbg3l0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBPdXQ%2FdJMcabLVicV%2F4VEepF0z8d02xvN9nbg3l0%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;3472&quot; height=&quot;410&quot; data-origin-width=&quot;3472&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 여러 차례 가비지 컬렉션을 통과했다는 것은 해당 객체가 잠깐 사용되는 임시 객체가 아닐 가능성이 높다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 일정 기준 이상 살아남은 객체는 Old Generation으로 이동할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;승격이란&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;승격(Promotion)&lt;/b&gt;은 Young Generation에서 살아남은 객체가 Old Generation으로 이동하는 것을 말합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4943&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OlgFb/dJMcaazqc7s/e89MXtEJY8Af18bNw9lZY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OlgFb/dJMcaazqc7s/e89MXtEJY8Af18bNw9lZY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OlgFb/dJMcaazqc7s/e89MXtEJY8Af18bNw9lZY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOlgFb%2FdJMcaazqc7s%2Fe89MXtEJY8Af18bNw9lZY1%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;4943&quot; height=&quot;305&quot; data-origin-width=&quot;4943&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 객체가 일정 나이에 도달하면 승격될 수 있습니다만, 실제 승격 시점은 단순히 객체 나이 하나로만 결정되지 않을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 조건도 영향을 줄 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Survivor 영역의 사용 가능한 공간&lt;/li&gt;
&lt;li&gt;살아남은 객체의 전체 크기&lt;/li&gt;
&lt;li&gt;객체의 나이 분포&lt;/li&gt;
&lt;li&gt;수집기 종류와 JVM 설정&lt;/li&gt;
&lt;li&gt;매우 큰 객체의 크기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Survivor 영역에 객체를 모두 저장할 수 없다면, 기준 나이에 도달하지 않은 객체도 Old Generation으로 이동할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Young GC에서 객체 생존
    &amp;darr;
Survivor 공간 확인
    &amp;darr;
공간 충분
&amp;rarr; Survivor로 이동

공간 부족
&amp;rarr; 일부 객체가 Old Generation으로 승격될 수 있음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체의 전체 이동 흐름&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;8191&quot; data-origin-height=&quot;1584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o8Ut4/dJMcagsPbHQ/JvrfgtJuKezXbFpNZ9p18K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o8Ut4/dJMcagsPbHQ/JvrfgtJuKezXbFpNZ9p18K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o8Ut4/dJMcagsPbHQ/JvrfgtJuKezXbFpNZ9p18K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo8Ut4%2FdJMcagsPbHQ%2FJvrfgtJuKezXbFpNZ9p18K%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;8191&quot; height=&quot;1584&quot; data-origin-width=&quot;8191&quot; data-origin-height=&quot;1584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 간단하게 표현하면 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;객체 생성
    &amp;darr;
Eden
    &amp;darr;
Young GC 생존
    &amp;darr;
Survivor
    &amp;darr;
여러 번 생존
    &amp;darr;
Old Generation
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 객체가 이 경로를 반드시 그대로 거치는 것은 아닙니다. 수집기와 객체 크기, 메모리 상황에 따라 객체가 Survivor를 충분히 거치지 않고 Old Generation으로 이동할 수도 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;세대를 나누면 무엇이 달라질까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세대를 나누는 가장 큰 이유는 수집 대상을 객체의 생존 특성에 따라 구분하기 위해서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세대를 나누지 않은 경우&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4485&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Btc9/dJMcaiqCyGb/F8GtHHsPTWPijAVGBAF1ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Btc9/dJMcaiqCyGb/F8GtHHsPTWPijAVGBAF1ok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Btc9/dJMcaiqCyGb/F8GtHHsPTWPijAVGBAF1ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Btc9%2FdJMcaiqCyGb%2FF8GtHHsPTWPijAVGBAF1ok%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;4485&quot; height=&quot;410&quot; data-origin-width=&quot;4485&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세대를 나눈 경우&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2650&quot; data-origin-height=&quot;990&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/or92w/dJMcafgqYko/3CVatkmJkPKDxzOt9ncmKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/or92w/dJMcafgqYko/3CVatkmJkPKDxzOt9ncmKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/or92w/dJMcafgqYko/3CVatkmJkPKDxzOt9ncmKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2For92w%2FdJMcafgqYko%2F3CVatkmJkPKDxzOt9ncmKk%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;525&quot; height=&quot;196&quot; data-origin-width=&quot;2650&quot; data-origin-height=&quot;990&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧게 살아남는 객체가 많은 Young Generation을 우선 수집하면 비교적 작은 범위를 대상으로 많은 가비지를 회수할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세대 구분의 효과&lt;/b&gt;&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 105px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;효과&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;수집 범위 축소&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;매번 전체 힙을 모두 탐색하지 않아도 됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;빠른 메모리 회수&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;짧게 사는 객체가 많은 영역을 집중적으로 수집할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;객체 특성별 관리&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;젊은 객체와 오래된 객체에 서로 다른 방식을 적용할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;전체 GC 부담 감소&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;오래 살아남은 객체를 매번 반복해서 처리하는 일을 줄일 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Young Generation이 작으면 무조건 좋을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young Generation이 작으면 한 번에 검사해야 하는 영역이 줄어들 수 있습니다. 따라서 한 번의 Young GC 정지 시간이 짧아질 가능성이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 공간이 빠르게 가득 차기 때문에 GC가 더 자주 발생할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Young Generation이 작음
    &amp;darr;
빠르게 가득 참
    &amp;darr;
Young GC가 자주 발생할 수 있음
    &amp;darr;
한 번의 정지는 짧아도 전체 GC 횟수 증가 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Young Generation이 크면 GC 발생 빈도는 줄어들 수 있지만, 한 번 수집할 때 처리해야 할 객체의 수가 많아질 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Young Generation 크기&lt;/td&gt;
&lt;td&gt;장점&lt;/td&gt;
&lt;td&gt;고려할 점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;작은 경우&lt;/td&gt;
&lt;td&gt;한 번의 수집 범위가 작을 수 있습니다.&lt;/td&gt;
&lt;td&gt;GC가 자주 발생할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;큰 경우&lt;/td&gt;
&lt;td&gt;객체를 더 많이 할당한 뒤 GC가 발생합니다.&lt;/td&gt;
&lt;td&gt;한 번의 수집 시간이 길어질 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Young Generation은 무조건 크거나 작게 만드는 것이 아니라, 애플리케이션의 객체 할당 속도와 응답 시간 요구사항에 맞게 조정해야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Old Generation의 수집 부담이 큰 이유&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Old Generation에는 여러 번의 가비지 컬렉션을 통과한 객체가 모입니다. 따라서 Young Generation과 비교하면 살아 있는 객체의 비율이 높을 가능성이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3021&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSVcZB/dJMcaccSJVp/HZMgqjuFTC5ytmjeRfzzd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSVcZB/dJMcaccSJVp/HZMgqjuFTC5ytmjeRfzzd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSVcZB/dJMcaccSJVp/HZMgqjuFTC5ytmjeRfzzd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSVcZB%2FdJMcaccSJVp%2FHZMgqjuFTC5ytmjeRfzzd1%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;3021&quot; height=&quot;410&quot; data-origin-width=&quot;3021&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉션은 가비지 객체만 찾는 것이 아니라 살아 있는 객체도 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Old Generation에 많은 객체가 존재하고 그중 상당수가 살아 있다면, 객체를 탐색하고 표시하거나 이동하는 비용이 커질 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Old Generation 객체 증가
    &amp;darr;
탐색해야 할 참조 증가
    &amp;darr;
살아 있는 객체 처리 비용 증가
    &amp;darr;
GC 부담과 정지 시간 증가 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 오래된 세대를 수집하는 작업은 일반적으로 Young Generation만 수집하는 작업보다 부담이 클 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;모든 가비지 컬렉터가 같은 구조를 사용할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eden, 두 개의 Survivor 영역, Old Generation으로 나누는 구조는 세대별 가비지 컬렉션을 이해하는 대표적인 모델입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 힙의 구조와 객체 이동 방식은 사용하는 가비지 컬렉터에 따라 달라질 수 있으며 요소는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;힙을 나누는 단위&lt;/li&gt;
&lt;li&gt;객체가 이동하는 방식&lt;/li&gt;
&lt;li&gt;Young 영역의 구성&lt;/li&gt;
&lt;li&gt;Old 영역을 수집하는 방식&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;예를 들어 어떤 수집기는 메모리를 물리적으로 연속된 Eden과 Old 영역으로 고정하지 않고, 여러 개의 작은 리전 단위로 나누어 역할을 부여하기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같이 이해하는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Eden &amp;rarr; Survivor &amp;rarr; Old

객체의 세대별 이동 원리를 이해하기 위한 대표적인 모델
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세부 구현은 수집기마다 달라질 수 있지만, 짧게 사는 객체와 오래 사는 객체를 구분해서 관리한다는 기본 목적은 같습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;약한 세대 가설과 세대별 힙의 관계&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;5962&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HwdJm/dJMcad3SLDa/g8bXjx5ADHKkQ8yrRrItC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HwdJm/dJMcad3SLDa/g8bXjx5ADHKkQ8yrRrItC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HwdJm/dJMcad3SLDa/g8bXjx5ADHKkQ8yrRrItC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHwdJm%2FdJMcad3SLDa%2Fg8bXjx5ADHKkQ8yrRrItC0%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;5962&quot; height=&quot;780&quot; data-origin-width=&quot;5962&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 105px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;약한 세대 가설의 관찰&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;힙 구조에 반영된 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;새 객체 대부분은 빠르게 사라집니다.&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;새 객체를 Young Generation에 모읍니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;일부 객체만 GC에서 살아남습니다.&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;살아남은 객체를 Survivor로 이동합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;여러 번 살아남은 객체는 계속 생존할 가능성이 높습니다.&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;일정 조건을 만족하면 Old Generation으로 승격합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;오래된 객체를 매번 검사하는 것은 비효율적입니다.&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Young과 Old를 구분해 서로 다른 주기로 수집합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핵심 정리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;STW는 일부 GC 작업을 위해 애플리케이션 스레드가 일시적으로 멈추는 상태입니다.&lt;/li&gt;
&lt;li&gt;객체의 실제 수명은 생성 시점에 정확히 알기 어렵습니다.&lt;/li&gt;
&lt;li&gt;약한 세대 가설은 많은 객체가 생성된 뒤 비교적 빠르게 사라진다는 관찰입니다.&lt;/li&gt;
&lt;li&gt;여러 번의 GC에서 살아남은 객체는 이후에도 계속 살아남을 가능성이 상대적으로 높습니다.&lt;/li&gt;
&lt;li&gt;전통적인 세대별 힙에서는 새 객체가 Eden에 할당됩니다.&lt;/li&gt;
&lt;li&gt;Young GC에서 살아남은 객체는 Survivor 영역으로 이동합니다.&lt;/li&gt;
&lt;li&gt;Survivor에서 계속 살아남은 객체는 Old Generation으로 승격될 수 있습니다.&lt;/li&gt;
&lt;li&gt;객체 나이는 실제 시간이 아니라 Young GC에서 살아남은 횟수와 관련된 값입니다.&lt;/li&gt;
&lt;li&gt;세대를 나누면 매번 전체 힙을 수집하지 않고 객체 수명 특성에 따라 다른 방식으로 관리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;실제 힙 구조와 객체 이동 방식은 가비지 컬렉터에 따라 달라질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙을 세대로 나누는 이유는 모든 객체의 수명이 같지 않기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 객체는 생성된 뒤 잠깐 사용되고 빠르게 접근할 수 없는 상태가 됩니다. 반면 일부 객체는 여러 번의 가비지 컬렉션을 통과하며 오랫동안 살아남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세대별 가비지 컬렉션은 이러한 차이를 이용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5382&quot; data-origin-height=&quot;984&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIbWT9/dJMcafHwjpP/cbQnccJJisjE408DE0QBnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIbWT9/dJMcafHwjpP/cbQnccJJisjE408DE0QBnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIbWT9/dJMcafHwjpP/cbQnccJJisjE408DE0QBnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIbWT9%2FdJMcafHwjpP%2FcbQnccJJisjE408DE0QBnK%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;5382&quot; height=&quot;984&quot; data-origin-width=&quot;5382&quot; data-origin-height=&quot;984&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧게 살아남는 객체는 Young Generation에서 빠르게 회수하고, 여러 번 살아남은 객체는 Old Generation으로 이동시켜 별도로 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 약한 세대 가설이 실제 가비지 컬렉션 구조에 반영된 대표적인 방식입니다.&lt;/p&gt;</description>
      <category>Development/JAVA</category>
      <author>KoreaNirsa</author>
      <guid isPermaLink="true">https://nirsa.tistory.com/492</guid>
      <comments>https://nirsa.tistory.com/492#entry492comment</comments>
      <pubDate>Sat, 25 Jul 2026 15:59:22 +0900</pubDate>
    </item>
    <item>
      <title>[Java GC] 핫스팟의 객체 표현과 가비지 컬렉션 루트</title>
      <link>https://nirsa.tistory.com/491</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핫스팟의 객체 표현과 가비지 컬렉션 루트&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉션은 힙에 생성된 객체 중 더 이상 사용할 수 없는 객체가 차지한 메모리를 회수합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 JVM은 힙에 있는 객체를 어떤 형태로 관리하고, 어떤 객체가 살아 있는지 어디서부터 확인할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 답하려면 다음 두 가지를 구분해서 이해해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;객체 표현
&amp;rarr; 힙에 생성된 객체가 어떤 구조로 저장되는가

객체 탐색
&amp;rarr; 가비지 컬렉터가 어디서부터 객체를 찾아가는가
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핫스팟 JVM에서는 힙의 객체를 내부적으로 OOP라는 구조를 통해 다룹니다. 객체의 앞부분에는 객체 상태와 클래스 정보를 담는 객체 헤더가 위치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉터는 객체 구조만 보고 생존 여부를 판단하지 않습니다. GC Root라고 부르는 시작점에서 객체 참조를 따라가며 도달 가능한 객체를 찾습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;핫스팟은 자바 애플리케이션을 실행하는 대표적인 JVM 구현체입니다.&lt;/li&gt;
&lt;li&gt;OOP는 핫스팟 내부에서 자바 객체를 가리키고 다루기 위한 표현입니다.&lt;/li&gt;
&lt;li&gt;객체 헤더는 Mark Word와 Klass Word로 구성됩니다.&lt;/li&gt;
&lt;li&gt;GC Root는 가비지 컬렉터가 살아 있는 객체를 찾기 시작하는 출발점입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핫스팟이란&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핫스팟(HotSpot)&lt;/b&gt;은 자바 바이트코드를 실행하는 대표적인 JVM 구현체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 소스 코드는 먼저 컴파일러에 의해 바이트코드로 변환됩니다. 이후 JVM이 바이트코드를 실행합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4592&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJwpJB/dJMcagNdkdC/KHFUPY903jjFE0tscoMKt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJwpJB/dJMcagNdkdC/KHFUPY903jjFE0tscoMKt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJwpJB/dJMcagNdkdC/KHFUPY903jjFE0tscoMKt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJwpJB%2FdJMcagNdkdC%2FKHFUPY903jjFE0tscoMKt1%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;4592&quot; height=&quot;305&quot; data-origin-width=&quot;4592&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM은 자바 프로그램 실행에 필요한 여러 기능을 담당합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 115px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기능&lt;/td&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;클래스 로딩&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;필요한 클래스 파일을 JVM 메모리에 적재합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;바이트코드 실행&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;컴파일된 자바 바이트코드를 실행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;JIT 컴파일&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;자주 실행되는 코드를 기계어로 컴파일합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;메모리 관리&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;힙, 스택 등의 런타임 메모리를 관리합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;가비지 컬렉션&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;사용하지 않는 객체의 메모리를 회수합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핫스팟이라는 이름은 프로그램에서 자주 실행되는 코드 영역인 &lt;b&gt;핫스팟&lt;/b&gt;을 찾아 최적화한다는 의미에서 붙었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 바이트코드를 실행하다가 특정 코드가 반복적으로 실행되는 것이 확인되면, JIT 컴파일러가 해당 코드를 운영체제와 CPU가 직접 실행할 수 있는 기계어로 변환합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4418&quot; data-origin-height=&quot;1066&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JSgDG/dJMcagTVauJ/kk1d0jKMQbNZjpoUuhRuY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JSgDG/dJMcagTVauJ/kk1d0jKMQbNZjpoUuhRuY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JSgDG/dJMcagTVauJ/kk1d0jKMQbNZjpoUuhRuY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJSgDG%2FdJMcagTVauJ%2Fkk1d0jKMQbNZjpoUuhRuY1%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;4418&quot; height=&quot;1066&quot; data-origin-width=&quot;4418&quot; data-origin-height=&quot;1066&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핫스팟은 자바 언어 자체가 아니라 &lt;b&gt;JVM 명세를 구현한 실행 환경&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이번 글에서 다루는 OOP, Mark Word, Klass Word와 같은 표현은 자바 언어 문법이라기보다 핫스팟 JVM 내부 구현과 관련된 개념입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핫스팟은 객체를 어떻게 표현할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 코드에서는 참조 변수를 사용해 객체에 접근합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User user = new User();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서 user는 객체 자체가 아닙니다. 힙에 생성된 User 객체를 가리키는 참조 변수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핫스팟 내부에서는 자바 객체를 가리키고 다루기 위한 표현으로 &lt;b&gt;OOP&lt;/b&gt;를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OOP는 일반적으로 &lt;b&gt;Ordinary Object Pointer&lt;/b&gt;를 의미합니다. 핫스팟 내부 코드에서는 객체를 가리키는 포인터 형태의 추상화로 사용됩니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;자바 코드 관점
참조 변수 &amp;rarr; 객체

핫스팟 내부 관점
OOP &amp;rarr; 힙에 존재하는 객체 구조
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 자바 개발자가 작성하는 참조 변수가 C나 C++의 포인터와 완전히 같다는 뜻은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 개발자가 객체의 실제 메모리 주소를 직접 계산하거나 변경할 수 없습니다. 객체 이동과 참조 갱신은 JVM이 관리합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;힙의 객체는 어떻게 구성될까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙에 생성되는 일반적인 인스턴스 객체는 크게 다음 영역으로 구성됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체 헤더&lt;/li&gt;
&lt;li&gt;인스턴스 필드&lt;/li&gt;
&lt;li&gt;정렬 패딩&lt;/li&gt;
&lt;/ul&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;3108&quot; data-origin-height=&quot;1255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uvoiV/dJMcahrNFWH/HBCZ9ZT0cHTfGKtsqiAvRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uvoiV/dJMcahrNFWH/HBCZ9ZT0cHTfGKtsqiAvRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uvoiV/dJMcahrNFWH/HBCZ9ZT0cHTfGKtsqiAvRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuvoiV%2FdJMcahrNFWH%2FHBCZ9ZT0cHTfGKtsqiAvRk%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;610&quot; height=&quot;246&quot; data-origin-width=&quot;3108&quot; data-origin-height=&quot;1255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;32비트 또는 64비트 JVM&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;li&gt;JVM 버전과 구현 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 객체 구조를 이해할 때는 고정된 바이트 크기보다 각 영역의 역할을 먼저 이해하는 것이 좋습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체 헤더에는 어떤 정보가 있을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;Mark Word&lt;/li&gt;
&lt;li&gt;Klass Word&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 객체는 객체 헤더에 배열 길이 정보가 추가됩니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;객체 구성&lt;/td&gt;
&lt;td&gt;주요 역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mark Word&lt;/td&gt;
&lt;td&gt;객체의 실행 상태와 GC 관련 정보를 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Klass Word&lt;/td&gt;
&lt;td&gt;객체가 어떤 클래스의 인스턴스인지 찾는 데 사용됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인스턴스 필드&lt;/td&gt;
&lt;td&gt;개발자가 클래스에 선언한 필드값을 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배열 길이&lt;/td&gt;
&lt;td&gt;배열이 가진 요소의 개수를 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배열 요소&lt;/td&gt;
&lt;td&gt;배열에 저장된 실제 데이터를 담습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정렬 패딩&lt;/td&gt;
&lt;td&gt;객체 크기를 JVM의 정렬 기준에 맞춥니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Mark Word란&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Mark Word&lt;/b&gt;는 객체의 실행 상태와 관련된 여러 정보를 저장하는 영역입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM의 상태와 객체의 현재 상황에 따라 다음과 같은 정보가 저장될 수 있습니다.&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;GC에서 사용하는 객체 나이&lt;/li&gt;
&lt;li&gt;마킹 상태와 관련된 정보&lt;/li&gt;
&lt;li&gt;모니터 또는 잠금 구조와 연결되는 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3315&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YDqAO/dJMb998hOcG/JoCscgHYYtkkaTWLBF4lN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YDqAO/dJMb998hOcG/JoCscgHYYtkkaTWLBF4lN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YDqAO/dJMb998hOcG/JoCscgHYYtkkaTWLBF4lN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYDqAO%2FdJMb998hOcG%2FJoCscgHYYtkkaTWLBF4lN1%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;656&quot; height=&quot;154&quot; data-origin-width=&quot;3315&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mark Word를 단순히 &lt;b&gt;객체 메타데이터를 가리키는 포인터&lt;/b&gt;라고 설명하는 것은 정확하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mark Word는 특정 객체의 상태를 표현하기 위한 비트 영역입니다. 객체가 잠겨 있는지, 해시 코드가 계산되었는지, GC 과정에서 어떤 상태인지 등에 따라 저장되는 내용의 해석이 달라질 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;※ Mark Word는 객체의 클래스 종류를 저장하는 영역이 아닙니다.&lt;br /&gt;객체 장금 상태, 해시 코드, GC 상태와 같은 객체 자체의 상태를 담는 영역입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 여기서 말하는 &lt;b&gt;Mark Word&lt;/b&gt;와 가비지 컬렉션의 &lt;b&gt;Mark 단계&lt;/b&gt;는 이름은 비슷하지만 같은 개념은 아닙니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mark Word&lt;/td&gt;
&lt;td&gt;객체 헤더에 포함된 데이터 영역입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC의 Mark 단계&lt;/td&gt;
&lt;td&gt;GC Root에서 도달 가능한 객체를 표시하는 과정입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Klass Word란&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Klass Word&lt;/b&gt;는 해당 객체가 어떤 클래스의 인스턴스인지 확인할 수 있도록 클래스 메타데이터와 연결하는 영역입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 힙에 User 객체가 생성되었다면, 객체의 Klass Word를 통해 해당 객체가 User 클래스의 인스턴스라는 정보를 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3641&quot; data-origin-height=&quot;1730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4tuMX/dJMcajiNQ0n/4y4q4V87OOvUHxGtzU26Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4tuMX/dJMcajiNQ0n/4y4q4V87OOvUHxGtzU26Nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4tuMX/dJMcajiNQ0n/4y4q4V87OOvUHxGtzU26Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4tuMX%2FdJMcajiNQ0n%2F4y4q4V87OOvUHxGtzU26Nk%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;631&quot; height=&quot;300&quot; data-origin-width=&quot;3641&quot; data-origin-height=&quot;1730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Klass 메타데이터에는 클래스를 실행하고 관리하기 위한 정보가 포함될 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스 이름과 타입 정보&lt;/li&gt;
&lt;li&gt;필드 정보&lt;/li&gt;
&lt;li&gt;메서드 정보&lt;/li&gt;
&lt;li&gt;상위 클래스 정보&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;Klass 메타데이터는 일반적인 자바 인스턴스 객체와 동일하게 힙에 저장되는 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대적인 핫스팟 JVM에서는 클래스 메타데이터가 주로 &lt;b&gt;메타스페이스(Metaspace)&lt;/b&gt;라는 네이티브 메모리 영역에 관리됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;2280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dw09Gf/dJMcaaTFW0h/ZCfAp3VNUFhn06AYFcy2U1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dw09Gf/dJMcaaTFW0h/ZCfAp3VNUFhn06AYFcy2U1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dw09Gf/dJMcaaTFW0h/ZCfAp3VNUFhn06AYFcy2U1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdw09Gf%2FdJMcaaTFW0h%2FZCfAp3VNUFhn06AYFcy2U1%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;447&quot; height=&quot;456&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;2280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Class&amp;lt;User&amp;gt;와 같이 자바 코드에서 다루는 java.lang.Class 객체는 힙에 존재할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음 두 개념을 구분해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&amp;nbsp;&lt;/td&gt;
&lt;td&gt;저장 위치와 역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Klass 메타데이터&lt;/td&gt;
&lt;td&gt;핫스팟 내부의 클래스 메타데이터이며 주로 메타스페이스에 존재합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;java.lang.Class 객체&lt;/td&gt;
&lt;td&gt;자바 코드에서 클래스 정보를 다루기 위한 객체이며 힙에 존재합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인스턴스 객체와 배열 객체의 차이&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 인스턴스 객체와 배열 객체는 모두 객체 헤더를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 배열은 몇 개의 요소를 가지고 있는지 JVM이 알아야 하므로 배열 길이 정보가 추가됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;인스턴스 객체&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2916&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JrAxl/dJMcajwlHYj/gADk4YBTSvZDHw3rB1jdIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JrAxl/dJMcajwlHYj/gADk4YBTSvZDHw3rB1jdIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JrAxl/dJMcajwlHYj/gADk4YBTSvZDHw3rB1jdIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJrAxl%2FdJMcajwlHYj%2FgADk4YBTSvZDHw3rB1jdIK%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;2916&quot; height=&quot;780&quot; data-origin-width=&quot;2916&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 객체의 데이터 영역에는 클래스에 선언한 필드값이 저장됩니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;class User {
    int age;
    String name;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User 객체에는 개념적으로 age 값과 name 참조가 인스턴스 필드 영역에 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;배열 객체&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3476&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVXAPm/dJMcajiNRgz/9m0zzlkLi0pnSzxEhYcTr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVXAPm/dJMcajiNRgz/9m0zzlkLi0pnSzxEhYcTr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVXAPm/dJMcajiNRgz/9m0zzlkLi0pnSzxEhYcTr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVXAPm%2FdJMcajiNRgz%2F9m0zzlkLi0pnSzxEhYcTr1%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;3476&quot; height=&quot;780&quot; data-origin-width=&quot;3476&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 객체는 다음 정보를 가집니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;배열의 객체 상태
배열의 타입
배열의 길이
배열의 각 요소
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 배열이 있다고 가정하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int[] numbers = new int[3];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적인 객체 구조는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Mark Word
Klass Word
배열 길이: 3
요소: 0, 0, 0
정렬 패딩
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;인스턴스 객체 vs 배열 객체&lt;/b&gt;&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;인스턴스 객체&lt;/td&gt;
&lt;td&gt;배열 객체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mark Word&lt;/td&gt;
&lt;td&gt;포함합니다.&lt;/td&gt;
&lt;td&gt;포함합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Klass Word&lt;/td&gt;
&lt;td&gt;포함합니다.&lt;/td&gt;
&lt;td&gt;포함합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;인스턴스 필드&lt;/td&gt;
&lt;td&gt;배열 요소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;길이 정보&lt;/td&gt;
&lt;td&gt;별도로 필요하지 않습니다.&lt;/td&gt;
&lt;td&gt;배열 길이를 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;크기&lt;/td&gt;
&lt;td&gt;필드 구성에 따라 결정됩니다.&lt;/td&gt;
&lt;td&gt;요소 타입과 개수에 따라 결정됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체 구조와 GC Root는 다른 개념입니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OOP와 객체 헤더는 &lt;b&gt;힙의 객체가 어떤 구조로 표현되는지&lt;/b&gt; 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 GC Root는 &lt;b&gt;가비지 컬렉터가 어떤 위치에서 객체 탐색을 시작하는지&lt;/b&gt; 설명합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2819&quot; data-origin-height=&quot;1730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekegUu/dJMcaa7hSrQ/xTKfn3KIWh82AkNU4bfLh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekegUu/dJMcaa7hSrQ/xTKfn3KIWh82AkNU4bfLh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekegUu/dJMcaa7hSrQ/xTKfn3KIWh82AkNU4bfLh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekegUu%2FdJMcaa7hSrQ%2FxTKfn3KIWh82AkNU4bfLh1%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;580&quot; height=&quot;356&quot; data-origin-width=&quot;2819&quot; data-origin-height=&quot;1730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;설명하는 대상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OOP&lt;/td&gt;
&lt;td&gt;핫스팟 내부에서 객체를 가리키는 표현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;객체 헤더&lt;/td&gt;
&lt;td&gt;개별 객체의 상태와 클래스 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC Root&lt;/td&gt;
&lt;td&gt;객체 생존 여부를 판단하기 위한 탐색 시작점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;참조 그래프&lt;/td&gt;
&lt;td&gt;객체들이 서로 연결된 관계&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 헤더가 있다고 해서 해당 객체가 반드시 살아 있는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 활성 객체인지 판단하려면 GC Root에서 해당 객체까지 도달할 수 있는지 확인해야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;가비지 컬렉션 루트란&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가비지 컬렉션 루트(GC Root)&lt;/b&gt;는 가비지 컬렉터가 살아 있는 객체를 찾기 위해 참조 탐색을 시작하는 출발점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC는 힙의 객체를 하나씩 보면서 사용할 것인지 질문하는 방식으로 동작하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 JVM이 현재 실행을 위해 반드시 유지해야 하는 참조들을 GC Root로 정합니다. 그다음 GC Root에서 객체 참조를 따라가며 도달 가능한 객체를 찾습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2321&quot; data-origin-height=&quot;1017&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn1xeZ/dJMcagGroQC/7tOiltPljChqxDmBKuF6y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn1xeZ/dJMcagGroQC/7tOiltPljChqxDmBKuF6y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn1xeZ/dJMcagGroQC/7tOiltPljChqxDmBKuF6y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn1xeZ%2FdJMcagGroQC%2F7tOiltPljChqxDmBKuF6y1%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;611&quot; height=&quot;268&quot; data-origin-width=&quot;2321&quot; data-origin-height=&quot;1017&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 구조에서는 GC Root에서 다음 객체에 도달할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체 A&lt;/li&gt;
&lt;li&gt;객체 B&lt;/li&gt;
&lt;li&gt;객체 C&lt;/li&gt;
&lt;li&gt;객체 D&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 네 객체는 활성 객체로 판단됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 X와 객체 Y는 서로를 참조하지만 GC Root에서 접근할 수 있는 경로가 없습니다.&lt;/p&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;객체 X ⇄ 객체 Y

GC Root에서 접근할 수 없음
&amp;rarr; Mark되지 않음
&amp;rarr; 가비지 컬렉션 대상
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GC Root가 필요한 이유&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙에 객체가 존재한다는 사실만으로는 객체가 실제로 사용 중인지 판단할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 두 객체는 모두 힙에 존재합니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;flowchart LR
    R[GC Root] --&amp;gt; A[객체 A]
    X[객체 X]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 두 객체의 상태는 다릅니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 63px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;객체힙에 존재&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;GC Root에서 도달 가능&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;판단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;객체 A&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;예&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;예&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;활성 객체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;객체 X&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;예&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;아니요&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;가비지 객체&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 X가 힙에 남아 있다고 해서 살아 있는 객체는 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉션에서 객체의 생존 여부를 결정하는 핵심 기준은 &lt;b&gt;도달 가능성(Reachability)&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;GC Root에서 도달 가능
&amp;rarr; 프로그램이 다시 사용할 가능성이 있음
&amp;rarr; 활성 객체

GC Root에서 도달 불가능
&amp;rarr; 프로그램에서 다시 접근할 방법이 없음
&amp;rarr; 가비지 객체
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 탐색 결과는 Mark and Sweep의 Mark 단계에서 어떤 객체를 표시할지 결정하는 기준이 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;주요 GC Root에는 무엇이 있을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC Root는 특정 객체 한 개를 의미하는 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM이 현재 프로그램 실행을 위해 유효하다고 판단하는 여러 참조의 집합입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 GC Root는 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GC Root 유형&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 중인 스레드의 스택 참조&lt;/td&gt;
&lt;td&gt;메서드의 지역 변수와 매개변수 등이 참조하는 객체입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU 레지스터의 참조&lt;/td&gt;
&lt;td&gt;현재 실행 중인 코드가 레지스터에 보관하고 있는 객체 참조입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정적 필드가 참조하는 객체&lt;/td&gt;
&lt;td&gt;로딩된 클래스의 static 필드가 참조하는 객체입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JNI 참조&lt;/td&gt;
&lt;td&gt;네이티브 코드에서 JNI를 통해 유지 중인 자바 객체입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 중인 스레드 객체&lt;/td&gt;
&lt;td&gt;JVM이 실행 중인 스레드를 관리하며 유지하는 참조입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVM 내부 참조&lt;/td&gt;
&lt;td&gt;클래스 로더, 시스템 클래스와 같이 JVM이 내부적으로 유지하는 객체입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 캐시의 참조&lt;/td&gt;
&lt;td&gt;JIT 컴파일된 코드가 포함하고 있는 객체 또는 메타데이터 참조입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;스택 프레임의 지역 변수&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public void process() {
    User user = new User();
    Order order = new Order();

    user.setOrder(order);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;process() 메서드가 실행 중이라면, 해당 메서드의 스택 프레임에 user와 order 참조가 존재할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2696&quot; data-origin-height=&quot;1130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xI8MV/dJMcaixrQJX/tKbTvyRWwtYvEQgFxC8f1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xI8MV/dJMcaixrQJX/tKbTvyRWwtYvEQgFxC8f1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xI8MV/dJMcaixrQJX/tKbTvyRWwtYvEQgFxC8f1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxI8MV%2FdJMcaixrQJX%2FtKbTvyRWwtYvEQgFxC8f1K%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;659&quot; height=&quot;276&quot; data-origin-width=&quot;2696&quot; data-origin-height=&quot;1130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 중인 스택 프레임의 참조는 GC 탐색의 출발점이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 User 객체와 Order 객체는 GC Root에서 도달 가능한 활성 객체입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정적 필드도 GC Root가 될 수 있을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 필드가 참조하는 객체도 살아 있는 객체 탐색의 출발점이 될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;public class UserCache {

    private static final Map&amp;lt;Long, User&amp;gt; CACHE = new HashMap&amp;lt;&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CACHE가 로딩된 클래스의 정적 필드에서 계속 참조되고 있다면, 그 안에 저장된 객체도 참조 관계를 통해 도달할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2403&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQlXxq/dJMcabSyvdv/jspzjKmKpK6iDzgAdbRyM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQlXxq/dJMcabSyvdv/jspzjKmKpK6iDzgAdbRyM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQlXxq/dJMcabSyvdv/jspzjKmKpK6iDzgAdbRyM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQlXxq%2FdJMcabSyvdv%2FjspzjKmKpK6iDzgAdbRyM0%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;586&quot; height=&quot;190&quot; data-origin-width=&quot;2403&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 객체는 일반적으로 GC Root에서 도달 가능하므로 가비지 컬렉션 대상이 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 정적 컬렉션에 객체를 계속 추가하고 제거하지 않으면 메모리 누수가 발생할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;static 컬렉션
    &amp;darr;
객체를 계속 참조
    &amp;darr;
GC Root에서 계속 도달 가능
    &amp;darr;
사용하지 않아도 수집되지 않음
    &amp;darr;
힙 사용량 지속 증가
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 말하는 메모리 누수는 메모리를 직접 해제하지 않아 발생하는 문제만을 의미하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램에서는 더 이상 필요하지 않지만, 참조가 남아 있어 GC가 회수하지 못하는 객체도 메모리 누수의 원인이 될 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;배열은 GC Root인가&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열은 그 자체만으로 일반적인 GC Root 유형이라고 보기는 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열도 힙에 생성되는 자바 객체이므로, 다른 객체와 마찬가지로 GC Root에서 도달 가능한지를 판단해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buqQEq/dJMcadJyejM/CQHpKagLTYOFbr0rFnp3O0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buqQEq/dJMcadJyejM/CQHpKagLTYOFbr0rFnp3O0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buqQEq/dJMcadJyejM/CQHpKagLTYOFbr0rFnp3O0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuqQEq%2FdJMcadJyejM%2FCQHpKagLTYOFbr0rFnp3O0%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;513&quot; height=&quot;222&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC Root가 배열을 참조하고 있다면 배열은 활성 객체입니다. 배열이 각 요소를 통해 다른 객체를 참조하고 있다면, 그 객체들도 배열을 거쳐 도달할 수 있으므로 활성 객체가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 배열을 가리키는 경로가 GC Root에서 모두 끊어지면 배열과 배열만을 통해 접근하던 객체도 가비지 컬렉션 대상이 될 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GC Root에서 객체를 탐색하는 과정&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;2541&quot; data-origin-height=&quot;1255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpzXrZ/dJMcagNdluc/JS6M56ZkgI2do1lxBVKuv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpzXrZ/dJMcagNdluc/JS6M56ZkgI2do1lxBVKuv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpzXrZ/dJMcagNdluc/JS6M56ZkgI2do1lxBVKuv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpzXrZ%2FdJMcagNdluc%2FJS6M56ZkgI2do1lxBVKuv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;272&quot; data-origin-width=&quot;2541&quot; data-origin-height=&quot;1255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉터는 GC Root에서 탐색을 시작합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;스택의 지역 변수
&amp;rarr; 객체 A
&amp;rarr; 객체 B
&amp;rarr; 객체 C

정적 필드
&amp;rarr; 객체 D
&amp;rarr; 객체 E
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 객체 A, B, C, D, E는 활성 객체로 Mark됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 X와 객체 Y는 서로를 참조하지만 GC Root에서 이어지는 경로가 없습니다. 따라서 Mark되지 않으며 메모리 회수 대상이 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;2890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9z4c8/dJMcadphdQ2/gkHzbZbqZ7f5lw6vQ8A2j0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9z4c8/dJMcadphdQ2/gkHzbZbqZ7f5lw6vQ8A2j0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9z4c8/dJMcadphdQ2/gkHzbZbqZ7f5lw6vQ8A2j0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9z4c8%2FdJMcadphdQ2%2FgkHzbZbqZ7f5lw6vQ8A2j0%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;254&quot; height=&quot;592&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;2890&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참조 개수보다 도달 가능성이 중요한 이유&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 여러 번 참조된다는 사실만으로 살아 있는 객체라고 판단할 수는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 두 객체가 서로를 참조할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1037&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AwZfP/dJMcajiNRLy/DOLhvMfQ3h0N9XEB6MbJmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AwZfP/dJMcajiNRLy/DOLhvMfQ3h0N9XEB6MbJmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AwZfP/dJMcajiNRLy/DOLhvMfQ3h0N9XEB6MbJmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAwZfP%2FdJMcajiNRLy%2FDOLhvMfQ3h0N9XEB6MbJmk%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;384&quot; height=&quot;113&quot; data-origin-width=&quot;1037&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 A는 객체 B의 참조를 받고 있고, 객체 B도 객체 A의 참조를 받고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조 개수만 보면 두 객체 모두 참조되고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 GC Root에서 두 객체까지 도달할 수 있는 경로가 없다면 실제 프로그램에서는 두 객체를 사용할 수 없습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;객체 A가 객체 B를 참조
객체 B가 객체 A를 참조

그러나 GC Root에서 접근 불가능
&amp;rarr; 두 객체 모두 가비지 객체
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 핫스팟의 가비지 컬렉션을 이해할 때는 참조 개수보다 &lt;b&gt;GC Root에서 출발한 참조 경로&lt;/b&gt;를 중심으로 봐야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체 표현부터 생존 판단까지의 전체 흐름&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;4921&quot; data-origin-height=&quot;2310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl4M4u/dJMcahk295C/x2mAZ96hty9j4qVAp4nZb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl4M4u/dJMcahk295C/x2mAZ96hty9j4qVAp4nZb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl4M4u/dJMcahk295C/x2mAZ96hty9j4qVAp4nZb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbl4M4u%2FdJMcahk295C%2Fx2mAZ96hty9j4qVAp4nZb0%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;4921&quot; height=&quot;2310&quot; data-origin-width=&quot;4921&quot; data-origin-height=&quot;2310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 헤더는 객체의 구조와 상태를 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC Root는 해당 객체가 현재 프로그램에서 필요한 객체인지 판단하기 위한 탐색의 출발점입니다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;객체 헤더
&amp;rarr; 이 객체가 어떤 객체이며 어떤 상태인가

GC Root
&amp;rarr; 이 객체에 현재 프로그램이 도달할 수 있는가
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핵심 정리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핫스팟은 자바 바이트코드를 실행하는 대표적인 JVM 구현체입니다.&lt;/li&gt;
&lt;li&gt;핫스팟은 자주 실행되는 코드를 찾아 JIT 컴파일을 통해 최적화합니다.&lt;/li&gt;
&lt;li&gt;OOP는 핫스팟 내부에서 힙의 자바 객체를 가리키고 다루기 위한 표현입니다.&lt;/li&gt;
&lt;li&gt;일반적인 객체는 객체 헤더, 객체 데이터, 정렬 패딩으로 구성됩니다.&lt;/li&gt;
&lt;li&gt;객체 헤더에는 Mark Word와 Klass Word가 포함됩니다.&lt;/li&gt;
&lt;li&gt;Mark Word에는 객체의 잠금 상태, 해시 코드, GC 관련 정보 등이 저장될 수 있습니다.&lt;/li&gt;
&lt;li&gt;Klass Word는 객체가 어떤 클래스의 인스턴스인지 확인할 수 있도록 Klass 메타데이터와 연결합니다.&lt;/li&gt;
&lt;li&gt;배열 객체에는 일반 객체 헤더와 함께 배열 길이 정보가 추가됩니다.&lt;/li&gt;
&lt;li&gt;GC Root는 가비지 컬렉터가 활성 객체를 찾기 시작하는 출발점입니다.&lt;/li&gt;
&lt;li&gt;실행 중인 스레드의 스택, 레지스터, 정적 필드, JNI 참조, JVM 내부 참조 등이 GC Root가 될 수 있습니다.&lt;/li&gt;
&lt;li&gt;객체의 생존 여부는 참조 개수가 아니라 GC Root에서 도달할 수 있는지에 따라 결정됩니다.&lt;/li&gt;
&lt;li&gt;배열은 그 자체가 GC Root라기보다 GC Root에서 도달 가능한 힙 객체로 이해해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핫스팟은 힙에 생성된 자바 객체를 OOP라는 내부 표현을 통해 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 객체의 앞부분에는 Mark Word와 Klass Word로 구성된 객체 헤더가 있습니다. Mark Word는 객체의 잠금 상태와 GC 관련 상태 등을 담고, Klass Word는 객체의 클래스 메타데이터를 찾는 데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 객체 구조만으로는 객체가 현재 사용 중인지 알 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉터는 스택의 지역 변수, 정적 필드, JNI 참조와 같은 GC Root에서 출발해 객체 참조를 따라갑니다. 이 과정에서 도달할 수 있는 객체는 활성 객체로 Mark하고, 도달할 수 없는 객체는 메모리 회수 대상으로 판단합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3281&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xMNF9/dJMcaiKS66P/t9zc6cAzMEGxLdgFt5NPMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xMNF9/dJMcaiKS66P/t9zc6cAzMEGxLdgFt5NPMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xMNF9/dJMcaiKS66P/t9zc6cAzMEGxLdgFt5NPMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxMNF9%2FdJMcaiKS66P%2Ft9zc6cAzMEGxLdgFt5NPMK%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;700&quot; height=&quot;166&quot; data-origin-width=&quot;3281&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 이해하면 가비지 컬렉션이 단순히 힙의 객체를 임의로 삭제하는 것이 아니라, JVM이 관리하는 명확한 출발점과 참조 관계를 기준으로 객체의 생존 여부를 판단한다는 점을 이해할 수 있습니다.&lt;/p&gt;</description>
      <category>Development/JAVA</category>
      <author>KoreaNirsa</author>
      <guid isPermaLink="true">https://nirsa.tistory.com/491</guid>
      <comments>https://nirsa.tistory.com/491#entry491comment</comments>
      <pubDate>Sat, 25 Jul 2026 15:28:20 +0900</pubDate>
    </item>
    <item>
      <title>[Java GC] 가비지 컬렉션은 왜 필요한가: 활성 객체와 Mark and Sweep</title>
      <link>https://nirsa.tistory.com/490</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;가비지 컬렉션은 왜 필요한가: 활성 객체와 Mark and Sweep&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 객체를 생성할 때 개발자가 직접 메모리를 할당하거나 반환하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User user = new User();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드처럼 new 키워드를 사용하면 객체가 생성되지만, C나 C++처럼 사용이 끝난 메모리를 직접 해제하는 코드는 작성하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 메모리 관리를 대신 수행하는 기능이 &lt;b&gt;가비지 컬렉션(Garbage Collection, GC)&lt;/b&gt;입니다.&lt;/p&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;JVM은 어떤 객체가 사용 중인지 어떻게 판단할까요?&lt;/li&gt;
&lt;li&gt;변수가 null이면 객체가 바로 제거될까요?&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;이 질문을 이해하려면 먼저 &lt;b&gt;활성 객체&lt;/b&gt;와 &lt;b&gt;가비지 객체&lt;/b&gt;를 구분해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 간단히 정리하면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;활성 객체는 현재 프로그램에서 접근할 수 있는 객체입니다.&lt;/li&gt;
&lt;li&gt;가비지&amp;nbsp;객체는&amp;nbsp;더&amp;nbsp;이상&amp;nbsp;접근할&amp;nbsp;수&amp;nbsp;없는&amp;nbsp;객체입니다.&lt;/li&gt;
&lt;li&gt;Mark and Sweep은 활성 객체를 표시한 뒤,&amp;nbsp;표시되지&amp;nbsp;않은&amp;nbsp;객체가&amp;nbsp;차지한&amp;nbsp;메모리를&amp;nbsp;회수합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체를 계속 만들기만 하면 어떤 문제가 생길까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램은 실행되는 동안 수많은 객체를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션을 예로 들면 요청 정보, 응답 데이터, 조회 결과, 문자열, 컬렉션과 같은 객체가 지속적으로 만들어집니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public String createMessage(String name) {
    String message = name + &quot;님, 환영합니다.&quot;;
    return message;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드가 실행될 때마다 문자열 객체가 생성될 수 있습니다. 하지만 모든 객체가 프로그램 종료 시점까지 필요한 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 객체는 잠시 사용된 뒤 더 이상 필요하지 않게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 사용이 끝난 객체가 힙 메모리에 계속 남아 있다면 다음과 같은 문제가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;객체 생성
   &amp;darr;
힙 메모리 사용량 증가
   &amp;darr;
사용이 끝난 객체도 계속 공간 차지
   &amp;darr;
새로운 객체를 저장할 공간 부족
   &amp;darr;
메모리 부족 또는 프로그램 성능 저하
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 JVM은 더 이상 사용할 수 없는 객체를 찾아 해당 객체가 차지했던 메모리를 다시 사용할 수 있도록 만들어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업을 수행하는 것이 가비지 컬렉션입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체가 힙에 존재하면 살아 있는 객체일까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 힙 메모리에 존재한다는 사실만으로는 살아 있는 객체라고 판단할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉션에서 중요한 기준은 &quot;현재 실행중인 프로그램이 해당 객체에 다시 접근할 수 있는가?&quot; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램에서 계속 접근할 수 있는 객체는 &lt;b&gt;활성 객체&lt;/b&gt;, 더 이상 접근할 수 없는 객체는 &lt;b&gt;가비지 객체&lt;/b&gt;로 구분할 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 63px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;의미&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;가비지 컬렉터의 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;활성 객체&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;프로그램에서 계속 접근할 수 있는 객체&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;메모리에 유지합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;가비지 객체&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;프로그램에서 더 이상 접근할 수 없는 객체&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;메모리 회수 대상이 됩니다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참조가 사라지면 객체는 어떻게 될까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class GarbageExample {

    public static void main(String[] args) {
        User user = new User(&quot;Kim&quot;);

        user = null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 user 변수가 User 객체를 참조합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;user ──────▶ User 객체
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서는 프로그램이 user 변수를 통해 객체에 접근할 수 있으므로 활성 객체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음 코드가 실행되면 참조 관계가 사라집니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;user = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;user ──────▶ null

              User 객체
              접근 경로 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기존의 User 객체에 접근할 수 있는 다른 참조가 없다면 해당 객체는 가비지 객체가 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 변수에 null을 대입했다고 해서 객체가 그 순간 즉시 메모리에서 제거되는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;객체에 대한 참조 제거
        &amp;darr;
객체에 접근할 수 없는 상태
        &amp;darr;
가비지 컬렉션 대상이 될 수 있음
        &amp;darr;
GC가 실행되는 시점에 메모리 회수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, null은 객체를 직접 삭제하는 명령이 아닙니다. 객체로 향하는 참조를 제거하는 표현에 가깝습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;활성 객체는 어떻게 찾을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM은 힙 메모리의 객체를 무작정 하나씩 살펴보며 사용 여부를 판단하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;가비지 컬렉션 루트(GC Root)&lt;/b&gt;라고 부르는 출발점에서 객체 참조를 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4707&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8H9sb/dJMcab58elZ/G3O9nkpOl5VDEU9Sffzimk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8H9sb/dJMcab58elZ/G3O9nkpOl5VDEU9Sffzimk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8H9sb/dJMcab58elZ/G3O9nkpOl5VDEU9Sffzimk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8H9sb%2FdJMcab58elZ%2FG3O9nkpOl5VDEU9Sffzimk%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;4707&quot; height=&quot;305&quot; data-origin-width=&quot;4707&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 구조에서 GC Root로부터 도달할 수 있는 객체는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;객체 A
객체 B
객체 C
객체 D
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 객체들은 프로그램이 계속 접근할 가능성이 있으므로 &lt;b&gt;활성 객체로 판단&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 객체 X와 객체 Y는 서로를 참조하고 있지만 GC Root에서 접근할 수 없습니다.&lt;/p&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;객체 X ⇄ 객체 Y
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 객체가 서로 참조하고 있다고 해도 프로그램의 유효한 출발점에서 접근할 수 없다면 가비지 객체가 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 자바의 가비지 컬렉션은 단순히 참조 개수만 세는 방식과는 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 기준은 &lt;b&gt;도달 가능성(Reachability)&lt;/b&gt;입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상태&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;td&gt;분류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC Root에서 참조 경로를 따라 도달 가능&lt;/td&gt;
&lt;td&gt;프로그램이 다시 사용할 수 있음&lt;/td&gt;
&lt;td&gt;활성 객체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC Root에서 어떤 경로로도 도달 불가능&lt;/td&gt;
&lt;td&gt;프로그램이 다시 사용할 수 없음&lt;/td&gt;
&lt;td&gt;가비지 객체&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC Root의 종류와 구체적인 탐색 방식은 별도의 글에서 자세히 다룰 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Mark and Sweep이란&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Mark and Sweep&lt;/b&gt;은 가비지 컬렉션의 기본 원리를 설명할 때 사용하는 대표적인 알고리즘입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 그대로 두 단계로 진행됩니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;Mark: 살아 있는 객체를 표시합니다.
Sweep: 표시되지 않은 객체의 공간을 회수합니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 가비지 컬렉터가 처음부터 가비지 객체를 직접 찾아다니는 것이 아니라는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 GC Root에서 도달 가능한 활성 객체를 찾습니다. 이후 활성 객체로 표시되지 않은 나머지 객체를 가비지 객체로 판단합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5812&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3ahNw/dJMcahSJhuG/QgpFOMjMdz9n1i5hXd9er1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3ahNw/dJMcahSJhuG/QgpFOMjMdz9n1i5hXd9er1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3ahNw/dJMcahSJhuG/QgpFOMjMdz9n1i5hXd9er1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3ahNw%2FdJMcahSJhuG%2FQgpFOMjMdz9n1i5hXd9er1%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;5812&quot; height=&quot;305&quot; data-origin-width=&quot;5812&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Mark 단계: 살아 있는 객체를 표시합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mark 단계에서는 GC Root에서 시작해 참조 관계를 따라가며 도달할 수 있는 객체를 찾습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 객체 그래프가 있다고 가정해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;1017&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blY2Aj/dJMcaaTFWhV/rhL8nMiSQvW42Vwnd39Ubk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blY2Aj/dJMcaaTFWhV/rhL8nMiSQvW42Vwnd39Ubk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blY2Aj/dJMcaaTFWhV/rhL8nMiSQvW42Vwnd39Ubk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblY2Aj%2FdJMcaaTFWhV%2FrhL8nMiSQvW42Vwnd39Ubk%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;421&quot; height=&quot;250&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;1017&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC Root에서 객체 A로 접근할 수 있고, 객체 A가 객체 B와 객체 C를 참조하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 객체 A, B, C는 모두 활성 객체로 표시됩니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;[Mark 완료]

객체 A: 표시됨
객체 B: 표시됨
객체 C: 표시됨
객체 X: 표시되지 않음
객체 Y: 표시되지 않음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 X와 객체 Y는 서로 연결되어 있지만 &lt;u&gt;GC Root에서 도달할 수 없으므로&lt;/u&gt; 활성 객체로 표시되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mark 단계의 목적은 객체를 즉시 제거하는 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;반드시 남겨야 할 객체를 확정하는 단계&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구분이 완료되어야 다음 단계에서 안전하게 메모리를 회수할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Sweep 단계: 표시되지 않은 공간을 회수합니다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sweep 단계에서는 Mark 단계에서 표시되지 않은 객체를 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표시되지 않았다는 것은 GC Root에서 도달할 수 없다는 의미이므로, 해당 객체가 차지했던 메모리는 회수 대상이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;Mark 단계 완료

[객체 A: 활성]
[객체 X: 가비지]
[객체 B: 활성]
[객체 Y: 가비지]
[객체 C: 활성]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sweep 단계 이후에는 다음과 같이 바뀔 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[객체 A: 활성]
[빈 공간]
[객체 B: 활성]
[빈 공간]
[객체 C: 활성]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회수된 공간은 이후 새로운 객체를 생성할 때 다시 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Order order = new Order();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 Order 객체가 생성되면 JVM은 회수된 메모리 공간을 새로운 객체 할당에 활용할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Mark and Sweep의 전체 처리 순서&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mark and Sweep의 흐름을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;6836&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqXaRe/dJMcaccSHt2/ZOEJBKtM88H2KTBTXdch81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqXaRe/dJMcaccSHt2/ZOEJBKtM88H2KTBTXdch81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqXaRe/dJMcaccSHt2/ZOEJBKtM88H2KTBTXdch81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqXaRe%2FdJMcaccSHt2%2FZOEJBKtM88H2KTBTXdch81%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;6836&quot; height=&quot;410&quot; data-origin-width=&quot;6836&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계별 역할은 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 118px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;단계&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;수행 내용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;탐색 시작&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;GC Root를 기준으로 객체 참조를 따라갑니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Mark&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;도달 가능한 활성 객체를 표시합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Sweep&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;표시되지 않은 객체가 차지한 공간을 회수합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;재사용&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;회수한 공간을 새로운 객체 할당에 사용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;객체를 회수했는데도 문제가 생길 수 있을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sweep 단계에서 가비지 객체의 메모리를 회수하면 사용 가능한 공간이 늘어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 회수된 공간이 힙의 여러 위치에 나뉘어 있을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;[활성 객체][빈 공간][활성 객체][빈 공간][활성 객체]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 가능한 빈 공간의 총합이 충분하더라도, 각각의 공간이 작은 크기로 흩어져 있으면 큰 객체를 연속된 공간에 배치하기 어려울 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 상태를 &lt;b&gt;메모리 단편화(Fragmentation)&lt;/b&gt;라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같이 2MB 크기의 빈 공간이 세 곳에 있다고 가정해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2MB 빈 공간 + 2MB 빈 공간 + 2MB 빈 공간
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 빈 공간은 6MB입니다. 하지만 5MB 크기의 연속된 메모리가 필요한 객체는 배치하기 어려울 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;총 빈 공간: 6MB
연속된 최대 빈 공간: 2MB
새 객체 크기: 5MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 완화하기 위해 살아남은 객체를 한쪽으로 이동시키는 &lt;b&gt;압축(Compaction)&lt;/b&gt;을 수행할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;압축은 왜 필요할까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축은 살아남은 객체를 한쪽으로 모아 빈 공간을 연속적으로 만드는 처리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축 전의 메모리가 다음과 같다고 가정해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;[객체 A][빈 공간][객체 B][빈 공간][객체 C][빈 공간]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축 후에는 다음과 같은 구조가 될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[객체 A][객체 B][객체 C][연속된 빈 공간]&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;3237&quot; data-origin-height=&quot;1530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvCm1U/dJMcacKN6o2/k38KEJhGTzUcib0XpmGltk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvCm1U/dJMcacKN6o2/k38KEJhGTzUcib0XpmGltk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvCm1U/dJMcacKN6o2/k38KEJhGTzUcib0XpmGltk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvCm1U%2FdJMcacKN6o2%2Fk38KEJhGTzUcib0XpmGltk%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;3237&quot; height=&quot;1530&quot; data-origin-width=&quot;3237&quot; data-origin-height=&quot;1530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축을 수행하면 큰 객체를 할당할 수 있는 연속된 공간을 확보하기 쉬워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 객체를 이동시키고 참조 주소를 조정해야 하므로 추가 비용이 발생할 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 105px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Sweep&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Compacion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;목적&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;가비지 객체의 공간을 회수합니다.&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;살아남은 객체를 한쪽으로 모읍니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;처리 결과&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;빈 공간이 여러 위치에 생길 수 있습니다.&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;연속된 빈 공간을 확보할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;주요 효과&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;사용 가능한 메모리를 늘립니다.&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;메모리 단편화를 완화합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;비용&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;객체 공간을 회수하는 비용이 발생합니다.&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;객체 이동과 참조 갱신 비용이 발생할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mark and Sweep과 압축을 결합한 방식을 일반적으로 &lt;b&gt;Mark-Compact&lt;/b&gt; 계열의 처리로 구분하기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Sweep과 Compaction은 완전히 같은 단계가 아닙니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Mark and Sweep
활성 객체 표시 &amp;rarr; 가비지 객체 공간 회수

Mark and Compact
활성 객체 표시 &amp;rarr; 살아남은 객체 이동 &amp;rarr; 연속된 공간 확보
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;순환 참조 객체도 수집할 수 있을까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 두 객체가 서로를 참조한다고 가정해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;class Node {
    Node next;
}

Node nodeA = new Node();
Node nodeB = new Node();

nodeA.next = nodeB;
nodeB.next = nodeA;

nodeA = null;
nodeB = null;
&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;2045&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LoCSk/dJMcaiqCvPH/C1egStlbezRCjyg5mDMXx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LoCSk/dJMcaiqCvPH/C1egStlbezRCjyg5mDMXx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LoCSk/dJMcaiqCvPH/C1egStlbezRCjyg5mDMXx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLoCSk%2FdJMcaiqCvPH%2FC1egStlbezRCjyg5mDMXx0%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;543&quot; height=&quot;207&quot; data-origin-width=&quot;2045&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 nodeA와 nodeB에 null을 대입하면 지역 변수에서 객체로 향하는 참조가 사라집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOz8WY/dJMcah6pYwU/3lNu1mvR0k0kGrvK0YrYo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOz8WY/dJMcah6pYwU/3lNu1mvR0k0kGrvK0YrYo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOz8WY/dJMcah6pYwU/3lNu1mvR0k0kGrvK0YrYo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOz8WY%2FdJMcah6pYwU%2F3lNu1mvR0k0kGrvK0YrYo1%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;425&quot; height=&quot;91&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 객체는 여전히 서로를 참조하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 GC Root에서 두 객체로 도달할 수 있는 경로가 없다면 두 객체 모두 가비지 객체로 판단할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서로 참조하고 있는가&lt;/b&gt; : 예&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GC Root에서 도달 가능한가&lt;/b&gt; : 아니요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결론&lt;/b&gt; : 가비지 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 도달 가능성을 기준으로 객체를 판단하는 방식의 중요한 특징입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Mark and Sweep을 이해할 때 주의할 점&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mark and Sweep은 가비지 컬렉션의 핵심 원리를 이해하는 데 적합한 모델입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 JVM의 가비지 컬렉터가 항상 단순한 두 단계만 수행하는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 가비지 컬렉터는 다음과 같은 요소를 함께 고려할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체가 생성된 영역&lt;/li&gt;
&lt;li&gt;객체가 살아남은 시간&lt;/li&gt;
&lt;li&gt;애플리케이션 정지 시간&lt;/li&gt;
&lt;li&gt;객체 이동 여부&lt;/li&gt;
&lt;li&gt;메모리 단편화&lt;/li&gt;
&lt;li&gt;병렬 처리와 동시 처리&lt;/li&gt;
&lt;li&gt;처리량과 응답 지연의 균형&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Mark and Sweep은 자바의 모든 가비지 컬렉터를 그대로 설명하는 완전한 구현 방식이라기보다, 다음 원리를 이해하기 위한 출발점에 가깝습니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;도달 가능한 객체를 찾습니다.
        &amp;darr;
살아 있는 객체를 구분합니다.
        &amp;darr;
도달할 수 없는 객체의 공간을 회수합니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핵심 정리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;객체의 생존 여부를 판단하는 핵심 기준은 GC Root에서의 도달 가능성입니다.&lt;/li&gt;
&lt;li&gt;GC Root에서 도달할 수 있는 객체는 활성 객체로 유지됩니다.&lt;/li&gt;
&lt;li&gt;GC Root에서 도달할 수 없는 객체는 가비지 객체가 될 수 있습니다.&lt;/li&gt;
&lt;li&gt;Mark 단계에서는 살아 있는 객체를 표시합니다.&lt;/li&gt;
&lt;li&gt;Sweep 단계에서는 표시되지 않은 객체가 차지한 메모리를 회수합니다.&lt;/li&gt;
&lt;li&gt;객체가 서로 순환 참조하더라도 GC Root에서 도달할 수 없다면 수집 대상이 될 수 있습니다.&lt;/li&gt;
&lt;li&gt;Sweep 이후 빈 공간이 흩어지면 메모리 단편화가 발생할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Compaction은 살아남은 객체를 한쪽으로 이동시켜 연속된 빈 공간을 확보하는 처리입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉션의 핵심은 단순히 객체를 삭제하는 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM은 먼저 현재 프로그램이 접근할 수 있는 객체를 찾아 활성 객체로 판단합니다. 그다음 활성 객체로 표시되지 않은 객체가 차지한 메모리를 회수합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 가장 기본적인 형태로 표현한 알고리즘이 Mark and Sweep입니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;GC Root에서 객체 탐색
        &amp;darr;
활성 객체 Mark
        &amp;darr;
표시되지 않은 객체 확인
        &amp;darr;
메모리 공간 Sweep
        &amp;darr;
새로운 객체 할당에 재사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Development/JAVA</category>
      <author>KoreaNirsa</author>
      <guid isPermaLink="true">https://nirsa.tistory.com/490</guid>
      <comments>https://nirsa.tistory.com/490#entry490comment</comments>
      <pubDate>Sat, 25 Jul 2026 14:33:36 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] BCrypt Salt의 이해: 로그인 검증이 가능한 이유와 Pepper를 고려하는 상황</title>
      <link>https://nirsa.tistory.com/489</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;[Spring&amp;nbsp;Security]&amp;nbsp;BCrypt&amp;nbsp;Salt의&amp;nbsp;이해:&amp;nbsp;로그인&amp;nbsp;검증이&amp;nbsp;가능한&amp;nbsp;이유와&amp;nbsp;Pepper를&amp;nbsp;고려하는&amp;nbsp;상황&lt;/blockquote&gt;
&lt;p data-end=&quot;116&quot; data-start=&quot;82&quot; data-ke-size=&quot;size16&quot;&gt;비밀번호 저장을 처음 구현할 때 흔히 &quot;BCryp로 암호화해서 저장한다&quot;라고 표현하지만 정확히는 아래와 같은 단방향 해싱 흐름입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[회원가입] 사용자 비밀번호 &amp;rarr; BCrypt 해싱 &amp;rarr; DB 저장
[로그인] 사용자 입력 비밀번호 &amp;rarr; DB의 BCrypt 해시와 비교&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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-end=&quot;246&quot; data-start=&quot;215&quot; data-ke-size=&quot;size16&quot;&gt;그런데 BCrypt를 조금 더 들여다보면 BCrypt는 매번 랜덤 Salt를 생성하므로 같은 비밀번호도 매번 다른 Hash가 나오게 됩니다. 그렇다면 로그인이 성공 가능한 이유는 무엇일지 의문이 생깁니다.&lt;/p&gt;
&lt;p data-end=&quot;354&quot; data-start=&quot;336&quot; data-ke-size=&quot;size16&quot;&gt;이번 글은 위의 질문에서 출발하며 먼저 요약한 결론은 다음과 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. BCrypt는 암호화가 아니라 단방향 해시다.
2. BCrypt는 매번 랜덤 Salt를 생성한다.
3. Salt는 최종 Hash 문자열 안에 함께 저장된다.
4. 로그인 시에는 새 Salt를 만들지 않고, 저장된 Hash 안의 Salt를 재사용한다.
5. DB가 유출되면 Salt와 Hash가 함께 노출된다.
6. 따라서 오프라인 딕셔너리 공격은 가능하다.
7. 이를 완화하기 위해 서버 측 비밀 값인 Pepper를 추가로 사용할 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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-end=&quot;865&quot; data-start=&quot;632&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security 공식 문서에서도 BCrypt는 Salt를 내장 생성하고, 생성된 Salt를 출력 Hash에 포함한다고 설명합니다. 또한 Spring Security의 BCryptPasswordEncoder는 기본적으로 strength 값 10을 사용하며, version, strength, SecureRandom을 설정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WWBOT/dJMcabdjjfs/xC3HSTUthNm4TGfYl2m63k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WWBOT/dJMcabdjjfs/xC3HSTUthNm4TGfYl2m63k/img.png&quot; data-alt=&quot;https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WWBOT/dJMcabdjjfs/xC3HSTUthNm4TGfYl2m63k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWWBOT%2FdJMcabdjjfs%2FxC3HSTUthNm4TGfYl2m63k%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;676&quot; height=&quot;274&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;BCrypt는 암호화가 아니라 Hash 이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 아직도 &quot;비밀번호를 암호화해서 저장한다&quot;는 표현을 사용하는 경우가 많지만 정확히는 해시로 저장한다고 표현하는 것이 맞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 저장의 목적은 원문을 복원하는 것이 아니라 사용자가 입력한 값이 기존에 저장된 값과 동일한지를 검증하는 것이기 때문입니다. &lt;br /&gt;&lt;br /&gt;따라서 비밀번호 저장에는 AES와 같은 양방향 암호화보다 BCrypt, Argon2, PBKDF2와 같은 단방향 Password Hashing 알고리즘이 사용됩니다. 용어는 아래와 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.6124%; text-align: center;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 33.6821%; text-align: center;&quot;&gt;암호화&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;일반 해시&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;Password Hashing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.6124%; text-align: center;&quot;&gt;대표 예시&lt;/td&gt;
&lt;td style=&quot;width: 33.6821%; text-align: center;&quot;&gt;AES, RSA&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;SHA-256, SHA-512&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;BCrypt, Argon2, PBKDF2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.6124%; text-align: center;&quot;&gt;방향&lt;/td&gt;
&lt;td style=&quot;width: 33.6821%; text-align: center;&quot;&gt;양방향&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;단방향&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;단방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.6124%; text-align: center;&quot;&gt;복호화&lt;/td&gt;
&lt;td style=&quot;width: 33.6821%; text-align: center;&quot;&gt;가능&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;불가능&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.6124%; text-align: center;&quot;&gt;비밀번호 저장&lt;/td&gt;
&lt;td style=&quot;width: 33.6821%; text-align: center;&quot;&gt;부적합&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;단독 사용 부적합&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.6124%; text-align: center;&quot;&gt;로그인 검증&lt;/td&gt;
&lt;td style=&quot;width: 33.6821%; text-align: center;&quot;&gt;복호화 후 비교&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;재계산 후 비교 가능&lt;/td&gt;
&lt;td style=&quot;width: 23.3527%; text-align: center;&quot;&gt;재계산 후 비교&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;단방향 해시라고 해서 모두 비밀번호 저장에 적합한 것은 아닙니다. &lt;/span&gt;&lt;span&gt;SHA-256, SHA-512 같은 일반 해시 함수는 계산이 너무 빠르기 때문에 &lt;/span&gt;&lt;span&gt;공격자가 대량의 후보 비밀번호를 빠르게 대입할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;따라서 비밀번호 저장에는 BCrypt, Argon2id, PBKDF2, scrypt처럼 &lt;/span&gt;&lt;span&gt;의도적으로 계산 비용을 높인 Password Hashing 알고리즘을 사용해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span&gt;Bcrypt 구조 파악하기&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BCryptPasswordEncoder의 코드를 직접 확인해보면 Salt 생성 후 Hash를 생성하는 모습과 저장된 Hash와 비교하여 확인하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgkvM0/dJMcafmy1d2/rlzNEd11OfSsgsr5vaFkOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgkvM0/dJMcafmy1d2/rlzNEd11OfSsgsr5vaFkOk/img.png&quot; data-alt=&quot;https://github.com/spring-projects/spring-security/blob/80dcf01ce5e755483db5078efd96393c5c557131/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgkvM0/dJMcafmy1d2/rlzNEd11OfSsgsr5vaFkOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgkvM0%2FdJMcafmy1d2%2FrlzNEd11OfSsgsr5vaFkOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;850&quot; height=&quot;451&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;451&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://github.com/spring-projects/spring-security/blob/80dcf01ce5e755483db5078efd96393c5c557131/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BCrypt를 보면 Salt 길이가 16바이트로 정의되어 있으며 getsalt(...) 내부에서는 아래와 같은 코드가 실행됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780538628497&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;byte rnd[] = new byte[BCRYPT_SALT_LEN];
random.nextBytes(rnd);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 실제 랜덤 Salt가 생성되는 지점입니다. SecureRandom을 통해 16바이트의 랜덤 데이터를 생성한 뒤, BCrypt 포맷($2a$10$...)에 맞게 버전(version), 비용 계수(cost factor), 그리고 Salt 값을 조합하여 문자열을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성일 기준 BCrypt 소스코드 686~709라인을 살펴보면 SecureRandom#nextBytes()로 랜덤 바이트를 생성한 후, 이를 BCrypt 전용 Base64 형식으로 인코딩하여 Salt 문자열에 포함시키는 과정을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BCrypt의 저장되는 문자열을 보통 $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy와 같이 작성되는데, 구조는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Version : $2a&lt;/li&gt;
&lt;li&gt;Cost : 10&lt;/li&gt;
&lt;li&gt;Salt : N9qo8uLOickgx2ZMRZoMye&lt;/li&gt;
&lt;li&gt;Hash : IjZAgcfl7p92ldGxad68LJZdL17lhWy&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 내부 BCrypt에서는 저장된 문자열에서 cost와 salt를 파싱합니다. 실제로 BCrypt의 650~653 라인을 확인해보면 22자의 Salt 문자열을 추출하고 이를 다시 디코딩하는 것을 확인해볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvF45R/dJMcab5u03i/N1oio9euQ4rJzfSuQp0PFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvF45R/dJMcab5u03i/N1oio9euQ4rJzfSuQp0PFK/img.png&quot; data-alt=&quot;https://github.com/spring-projects/spring-security/blob/80dcf01ce5e755483db5078efd96393c5c557131/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvF45R/dJMcab5u03i/N1oio9euQ4rJzfSuQp0PFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvF45R%2FdJMcab5u03i%2FN1oio9euQ4rJzfSuQp0PFK%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;630&quot; height=&quot;104&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://github.com/spring-projects/spring-security/blob/80dcf01ce5e755483db5078efd96393c5c557131/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Hash 결과를 만들 때는 Salt와 Hash를 하나의 문자열에 이어붙여 만들어지게 됩니다. 즉, BCrypt는 단순히 Hash만 저장하는 것이 아니라 검증에 필요한 모든 정보를 하나의 문자열 안에 포함하고 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;검증할 때는 새로운 Salt를 만들지 않는다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BCrypt의 방식에 의문이 든것은 아래와 같았습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;BCrypt는 매번 랜덤 Salt를 생성함&lt;/li&gt;
&lt;li&gt;로그인때도 새 Salt를 만들면 전체적인 Hash가 달라짐&lt;/li&gt;
&lt;li&gt;그렇다면 같은 값을 입력하더라도 로그인은 매번 실패됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로 코드를 살펴본 결과 검증을 할때는 새 Salt를 만들지 않았습니다. 스프링 시큐리티의 matchesNonNull()은 입력 비밀번호와 DB에 저장된 BCrypt 문자열을 BCrypt.checkpw()로 넘깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 코드는 아래와 같은데, encodedPassword는 DB에 저장된 전체 BCrypt 문자열입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780539144049&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return BCrypt.checkpw(rawPassword.toString(), encodedPassword);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BCrypt.checkpw()는 이 문자열에서 version, cost, salt를 다시 꺼내고 사용자가 입력한 평문 비밀번호(rawPassword.toString())를 같은 조건으로 다시 해쉬하여 비교합니다. 간단한 흐름은 아래와 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로그인 요청 : rawPassword = &quot;1234&quot;&lt;/li&gt;
&lt;li&gt;DB 저장 값 : &lt;span&gt;$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;DB에 저장된 값에서 Salt 추출&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;입력된 비밀번호(rawPassword)를 추출한 Salt로 BCrypt 재계산&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;재계산 결과와 DB 저장값 비교&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 BCrypt는 처음 저장 시 Salt를 랜덤 생성하여 Salt+입력값으로 해시를 만들어 저장하고, 추후 검증이 필요할 경우 Salt값을 가져와 입력값으로 해싱하여 기존 값과 비교하는 구조를 가집니다.&lt;/p&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;rawPassword = &quot;1234&quot;&lt;/li&gt;
&lt;li&gt;BCrypt가 랜덤 Salt 생성 후 해싱 -&amp;gt; A의 결과&lt;/li&gt;
&lt;li&gt;이후 BCrypt를 재실행하면 또 새로운 랜덤 Salt가 생성되며 해싱 -&amp;gt; B의 결과&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Salt가&amp;nbsp;없으면&amp;nbsp;공격자는&amp;nbsp;&quot;1234&quot;&amp;nbsp;&amp;rarr;&amp;nbsp;Hash&amp;nbsp;값처럼&amp;nbsp;미리&amp;nbsp;계산해&amp;nbsp;둔&amp;nbsp;테이블을&amp;nbsp;여러&amp;nbsp;DB에&amp;nbsp;재사용할&amp;nbsp;수&amp;nbsp;있습니다. &lt;br /&gt;하지만&amp;nbsp;사용자마다&amp;nbsp;Salt가&amp;nbsp;다르면&amp;nbsp;같은&amp;nbsp;&quot;1234&quot;라도&amp;nbsp;Salt마다&amp;nbsp;결과가&amp;nbsp;달라집니다. &lt;br /&gt;&lt;br /&gt;따라서 공격자는 기존에 만들어 둔 범용 Rainbow Table을 그대로 재사용하기 어렵고 각 사용자 Salt마다 후보 비밀번호를 다시 계산해야 합니다. 즉 Salt는 공격 자체를 불가능하게 만드는 값이라기보다 사전&amp;nbsp;계산&amp;nbsp;공격의&amp;nbsp;경제성을&amp;nbsp;크게&amp;nbsp;떨어뜨리는&amp;nbsp;값입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span&gt;Salt는 왜 공개되어도 되고, Cost Factor는 어떤 역할을 수행하는가?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사람들이 Salt도 숨겨야 하는 값이라고 생각하지만 Salt의 목적은 비밀 유지가 아닙니다. Salt는&amp;nbsp;동일한&amp;nbsp;비밀번호가&amp;nbsp;동일한&amp;nbsp;Hash로&amp;nbsp;저장되는&amp;nbsp;문제를&amp;nbsp;해결하기&amp;nbsp;위해&amp;nbsp;존재합니다. &lt;br /&gt;&lt;br /&gt;즉 공격자가 Salt를 알고 있다는 전제 하에 설계된 것이 BCrypt 이기 때문에&amp;nbsp;Salt를 Hash 내부에 그대로 저장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780540976779&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;1234&quot; &amp;rarr; Salt A &amp;rarr; Hash A 
&quot;1234&quot; &amp;rarr; Salt B &amp;rarr; Hash B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;BCrypt의 Cost Factor는 해시 계산에 필요한 연산량을 의미하며 &lt;/span&gt;&lt;span&gt;Cost가 1 증가할 때마다 필요한 연산량은 2배씩 증가합니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;COst&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;반복 시 발생하는 비용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;2^10 = 1,024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;11&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;2^11 =&lt;/span&gt; 2,048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;12&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;2^12 =&lt;span&gt; &lt;/span&gt;&lt;/span&gt;4,096&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;13&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;2^13 =&lt;/span&gt; 8,192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;14&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;2^14 =&lt;span&gt; &lt;/span&gt;&lt;/span&gt;16,384&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Salt는 레인보우 테이블 공격을 방어하고 Cost Factor는 무차별 대입 공격과 딕셔너리 공격의 비용을 증가시키는 역할을 수행합니다. 다만, Cost Factor는 높을수록 많은 연산이 필요하기에 서버의 부하가 발생할 수 있으므로 너무 높기만 해도 좋은 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cost는&amp;nbsp;높을수록&amp;nbsp;공격&amp;nbsp;비용을&amp;nbsp;증가시키지만,&amp;nbsp;로그인&amp;nbsp;서버의&amp;nbsp;CPU&amp;nbsp;비용도&amp;nbsp;함께&amp;nbsp;증가시킵니다. &lt;br /&gt;따라서 무조건 높은 값을 선택하기보다는 실제 서버 환경에서 측정한 뒤 사용자&amp;nbsp;경험과&amp;nbsp;서버&amp;nbsp;부하를&amp;nbsp;감당할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;수준으로&amp;nbsp;조정해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span&gt;BCrypt만 사용할 경우 DB 유출 시 어떤 일이 발생하는가&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 결론이 나옵니다. BCrypt가 레인보우 테이블 어택을 어렵게 만들지만, DB가 유출되면 오프라인 딕셔너리 공격 자체가 불가능해지는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB가 유출되면 공격자는 아래와 같은 정보를 얻습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780539559412&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 Version, Cost, Salt, Hash의 정보가 들어있기에 공격자는 비밀번호 후보를 하나씩 대입할 수 있습니다. 간략한 공격 흐름은 아래와 같습니다. 딕셔너리 어택은 자주 사용되는 단어, 문구, 숫자등을 조합한 사전(Disctionary)을 미리 만들어두고 이를 순차적으로 대입하는 해킹 기법이며 아래에서 언급하는 후보 비밀번호는 사전에 있는 하나의 문자열을 의미합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DB Hash 획득&lt;/li&gt;
&lt;li&gt;Hash 문자열에서 Salt만 추출&lt;/li&gt;
&lt;li&gt;후보 비밀번호 선택&lt;/li&gt;
&lt;li&gt;BCrypt(후보 비밀번호, 추출한 Salt)&lt;/li&gt;
&lt;li&gt;DB Hash와비교&lt;/li&gt;
&lt;li&gt;일치하면 비밀번호 추측 성공&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충분히 길고 예측 불가능한 비밀번호라면 BCrypt Hash를 깨는 것은 매우 어렵지만&amp;nbsp;실제 사용자의 비밀번호는 사전에 있는 단어, 생년월일, 전화번호, 서비스명 등을 조합하는 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;따라서 DB가 유출되면 공격자는 저장된 Salt와 Cost를 이용해 오프라인 딕셔너리 공격을 수행할 수 있고 약한&amp;nbsp;비밀번호는&amp;nbsp;여전히&amp;nbsp;추측될&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다만 이 논의는 기본적으로 &amp;ldquo;비밀번호 저장&amp;rdquo;에 대한 이야기입니다. &lt;/span&gt;주민등록번호, 전화번호, 사번, 학번처럼 후보 공간이 작거나 복원이 필요한 데이터는 비밀번호와&amp;nbsp;다른&amp;nbsp;방식으로&amp;nbsp;설계해야&amp;nbsp;합니다. &lt;br /&gt;&lt;br /&gt;원문 복원이 필요 없다면 서버 측 비밀 키를 사용하는 HMAC 기반 저장을 검토할 수 있고, 원문 복원이 필요하다면 KMS/Secret Manager로 관리되는 키를 사용한 필드 레벨 암호화가 더 적합할 수 있습니다.&lt;br /&gt;&lt;br /&gt;중요한&amp;nbsp;점은&amp;nbsp;Salt가&amp;nbsp;공개된&amp;nbsp;단순&amp;nbsp;Hash만으로는&amp;nbsp;후보&amp;nbsp;공간이&amp;nbsp;작은&amp;nbsp;데이터를&amp;nbsp;충분히&amp;nbsp;보호하기&amp;nbsp;어렵다는&amp;nbsp;것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span&gt;Salt와 Pepper&lt;/span&gt;&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 147px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.9147%; text-align: center; height: 21px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 32.8682%; text-align: center; height: 21px;&quot;&gt;Salt&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center; height: 21px;&quot;&gt;Pepper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.9147%; text-align: center; height: 21px;&quot;&gt;목적&lt;/td&gt;
&lt;td style=&quot;width: 32.8682%; text-align: center; height: 21px;&quot;&gt;Hash 중복 방지, 레인보우 테이블 방어&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center; height: 21px;&quot;&gt;DB 단독 유출 시 오프라인 크래킹 난이도 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.9147%; text-align: center; height: 21px;&quot;&gt;저장 위치&lt;/td&gt;
&lt;td style=&quot;width: 32.8682%; text-align: center; height: 21px;&quot;&gt;Hash 내부 또는 DB&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center; height: 21px;&quot;&gt;DB와 분리된 Secret Manager, Vault, KMS, HSM 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.9147%; text-align: center; height: 21px;&quot;&gt;공개 여부&lt;/td&gt;
&lt;td style=&quot;width: 32.8682%; text-align: center; height: 21px;&quot;&gt;공개 가능&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center; height: 21px;&quot;&gt;비공개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.9147%; text-align: center; height: 21px;&quot;&gt;사용자별&lt;/td&gt;
&lt;td style=&quot;width: 32.8682%; text-align: center; height: 21px;&quot;&gt;사용자마다 다름&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center; height: 21px;&quot;&gt;일반적으로 애플리케이션/시스템 단위로 공유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.9147%; text-align: center; height: 21px;&quot;&gt;DB 유출 시 노출&lt;/td&gt;
&lt;td style=&quot;width: 32.8682%; text-align: center; height: 21px;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center; height: 21px;&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.9147%; text-align: center; height: 21px;&quot;&gt;교체 난이도&lt;/td&gt;
&lt;td style=&quot;width: 32.8682%; text-align: center; height: 21px;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;width: 43.217%; text-align: center; height: 21px;&quot;&gt;높음. 기존 비밀번호 원문을 모르므로 로그인/비밀번호 재설정 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-end=&quot;11442&quot; data-start=&quot;11231&quot; data-ke-size=&quot;size16&quot;&gt;OWASP는 Pepper를 비밀번호 Salt처럼 공개하거나 Hash와 함께 저장하면 안 되고 DB와 분리된 Secret Vault나 HSM에 저장해야 한다고 설명합니다. 또한 Pepper가 노출되면 사용자의 원문 비밀번호를 알 수 없기 때문에 Pepper 교체에는 비밀번호 재설정이 필요하다고 설명합니다.&lt;/p&gt;
&lt;p data-end=&quot;11626&quot; data-start=&quot;11444&quot; data-ke-size=&quot;size16&quot;&gt;추가로 OWASP Cryptographic Storage Cheat Sheet는 키를 소스 코드에 하드코딩하거나 버전 관리 시스템에 올리지 말고, 가능하면 KMS, Key Vault, HSM, Vault 같은 별도 키 관리 시스템을 사용하라고 안내합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Pepper를 추가하면 무엇이 달라질까&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서버에서 password + pepper 형태로 저장했다고 가정한다면 DB가 유출되더라도 공격자는 Pepper 값을 모르므로 Salt를 알고, 실제 사용된 값으로 해싱하더라도 동일한 해시를 재현할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 아래와 같은 코드가 존재한다면 DB가 유출되엇을 때 $2a$12$N9qo8uLOickgx2ZMRZoMye...와 같은 값을 가져가 Salt를 알 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780541257695&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String encoded = passwordEncoder.encode(rawPassword + pepper);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 pepper를 사용했을 경우 &quot;password123&quot; + &quot;pepper값&quot;이 붙기에 공격자는 pepper를 추측할 수 없어서 동일한 입력값을 넣더라도 같은 해시를 확인할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 &quot;password123&quot;를 해싱하였다면 공격자는 DB 유출 된 데이터를 기반으로 Salt + &quot;password123&quot;으로 동일한 해시값을 얻어서 추측이 가능하지만, &quot;password123secret-pepper&quot;를 해싱했을 경우 공격자는 Salt + &quot;password123&quot;으로 동일한 해시값을 얻어낼 수 없습니다. 실제 사용자의 비밀번호 뒤에 pepper가 추가적으로 붙어서 해싱된 값이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 DB와 함께 서버의 코드 또는 환경 변수, Secret Manager가 노출되었을 경우 공격자는 pepper의 값도 알아낼 수 있기 때문에 만능은 아닙니다. pepper가 유효한 것은 DB 데이터만 유출되었을 때로 한정됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;Pepper를 추가할 때 단순 문자열 연결만으로 충분할까?&lt;/blockquote&gt;
&lt;p data-end=&quot;138&quot; data-start=&quot;97&quot; data-ke-size=&quot;size16&quot;&gt;에서는 이해를 돕기 위해 아래와 같은 형태로 Pepper를 설명했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;String encoded = passwordEncoder.encode(rawPassword + pepper);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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-end=&quot;292&quot; data-start=&quot;216&quot; data-ke-size=&quot;size16&quot;&gt;개념적으로는 맞습니다. 사용자가 입력한 비밀번호에 서버만 알고 있는 비밀값인 Pepper를 추가한 뒤 BCrypt로 해싱하는 구조입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;rawPassword + pepper &amp;rarr; BCrypt(rawPassword + pepper, random salt, cost) &amp;rarr; DB 저장&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;673&quot; data-start=&quot;422&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 DB만 유출된 상황에서는 공격자가 Salt와 Hash를 모두 알고 있더라도 Pepper 값을 모르기 때문에 동일한 BCrypt 입력값을 재현하기 어려워집니다. OWASP도 Pepper를 Salt와 함께 사용할 수 있는 추가 방어층으로 설명하며, Pepper는 Hash와 함께 저장하지 말고 DB와 분리된 Secret Vault나 HSM 등에 보관해야 한다고 설명합니다.&lt;br /&gt;&lt;br /&gt;하지만 실무에서는 단순히 rawPassword + pepper를 그대로 BCrypt에 넣는 방식에는 주의가 필요합니다.&lt;br /&gt;&lt;br /&gt;가장 큰 이유는 BCrypt의 입력 길이 제한입니다. OWASP Password Storage Cheat Sheet는 bcrypt 사용 시 72바이트 입력 제한을 고려해야 한다고 설명합니다. Spring Security도 2025년에 BCryptPasswordEncoder의 72자 초과 비밀번호 처리와 관련된 보안 권고를 발표한 바 있습니다. (&lt;a href=&quot;https://spring.io/security/cve-2025-22228&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CVE-2025-2228&lt;/a&gt;)&lt;br /&gt;&lt;br /&gt;예를 들어 아래와 같은 입력이 있다고 가정해보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;rawPassword = &quot;매우 긴 비밀번호...&quot;
pepper      = &quot;server-secret-pepper&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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-end=&quot;1226&quot; data-start=&quot;1128&quot; data-ke-size=&quot;size16&quot;&gt;BCrypt 구현체가 입력 앞부분 72바이트까지만 사용한다면, 비밀번호가 이미 72바이트를 초과하는 순간 뒤에 붙인 Pepper가 실제 해싱 입력에 반영되지 않을 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;rawPassword + pepper &amp;rarr; BCrypt는 앞 72바이트까지만 사용 &amp;rarr; pepper가 잘릴 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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-end=&quot;1414&quot; data-start=&quot;1340&quot; data-ke-size=&quot;size16&quot;&gt;따라서 실무에서는 단순 문자열 연결보다는 HMAC 기반 pre-hash 후 BCrypt를 적용하는 방식을 고려할 수 있습니다. 흐름은 아래와 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[Pre-hash Peppering]
사용자 입력 비밀번호 &amp;rarr; HMAC-SHA384(rawPassword, pepperKey) &amp;rarr; Base64 인코딩 &amp;rarr; BCrypt(Base64(HMAC 결과), random salt, cost) &amp;rarr; DB 저장&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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-end=&quot;1779&quot; data-start=&quot;1654&quot; data-ke-size=&quot;size16&quot;&gt;이 방식에서는 Pepper를 단순히 문자열로 이어붙이지 않고 HMAC의 Secret Key로 사용합니다. HMAC 결과를 Base64로 인코딩하면 고정 길이 문자열이 만들어지고 이 값을 BCrypt의 입력으로 사용합니다.&lt;/p&gt;
&lt;p data-end=&quot;1935&quot; data-start=&quot;1781&quot; data-ke-size=&quot;size16&quot;&gt;OWASP도 bcrypt와 pre-hashing을 함께 사용할 경우 bcrypt(base64(hmac-sha384(password, pepper)), salt, cost) 형태의 접근을 예시로 제시합니다.&lt;/p&gt;
&lt;p data-end=&quot;1935&quot; data-start=&quot;1781&quot; data-ke-size=&quot;size16&quot;&gt;만약, 스프링부트에서 해당 방식을 적용한다면 아래와 같은 코드와 같이 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780546398570&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.security.crypto.password.PasswordEncoder;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.Base64;
import java.util.Objects;

public final class PepperingPasswordEncoder implements PasswordEncoder {

    private static final String HMAC_ALGORITHM = &quot;HmacSHA384&quot;;

    private final PasswordEncoder delegate;
    private final byte[] pepperKey;

    public PepperingPasswordEncoder(PasswordEncoder delegate, byte[] pepperKey) {
        this.delegate = Objects.requireNonNull(delegate);
        this.pepperKey = Objects.requireNonNull(pepperKey).clone();
    }

    @Override
    public String encode(CharSequence rawPassword) {
        return delegate.encode(preHash(rawPassword));
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return delegate.matches(preHash(rawPassword), encodedPassword);
    }

    private String preHash(CharSequence rawPassword) {
        try {
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            SecretKeySpec keySpec = new SecretKeySpec(pepperKey, HMAC_ALGORITHM);
            mac.init(keySpec);

            byte[] hmac = mac.doFinal(
                    rawPassword.toString().getBytes(StandardCharsets.UTF_8)
            );

            return Base64.getEncoder().encodeToString(hmac);
        } catch (GeneralSecurityException e) {
            throw new IllegalStateException(&quot;Failed to calculate password HMAC&quot;, e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780546407417&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.Base64;

@Configuration
public class PasswordEncoderConfig {

    @Bean
    public PasswordEncoder passwordEncoder(
            @Value(&quot;${security.password.pepper-key}&quot;) String base64PepperKey
    ) {
        byte[] pepperKey = Base64.getDecoder().decode(base64PepperKey);

        return new PepperingPasswordEncoder(
                new BCryptPasswordEncoder(12),
                pepperKey
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4796&quot; data-start=&quot;4757&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 구성하면 회원가입과 로그인 검증 흐름은 아래처럼 동작합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[회원가입]
rawPassword &amp;rarr; HMAC-SHA384(rawPassword, pepperKey) &amp;rarr; Base64(HMAC 결과) &amp;rarr; BCryptPasswordEncoder.encode() &amp;rarr; DB 저장&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[로그인]
rawPassword &amp;rarr; HMAC-SHA384(rawPassword, pepperKey) &amp;rarr; Base64(HMAC 결과) &amp;rarr; BCryptPasswordEncoder.matches() &amp;rarr; DB의 BCrypt Hash와 비교&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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-end=&quot;5244&quot; data-start=&quot;5152&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 서비스 코드에서는 기존과 동일하게 passwordEncoder.encode()와 passwordEncoder.matches()만 사용하면 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;String encodedPassword = passwordEncoder.encode(rawPassword);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;boolean result = passwordEncoder.matches(rawPassword, storedPasswordHash);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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-end=&quot;5452&quot; data-start=&quot;5409&quot; data-ke-size=&quot;size16&quot;&gt;즉, Pepper 적용 여부를 서비스 로직에서 매번 신경 쓰지 않아도 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span&gt;마무리&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 BCrypt의 Salt는 숨기기 위한 값이 아니라 동일한 비밀번호가 동일한 Hash로 저장되는 것을 방지하고 Rainbow&amp;nbsp;Table과&amp;nbsp;같은&amp;nbsp;사전&amp;nbsp;계산&amp;nbsp;공격의&amp;nbsp;효율을&amp;nbsp;떨어뜨리기&amp;nbsp;위한&amp;nbsp;값입니다. &lt;br /&gt;&lt;br /&gt;BCrypt는 Salt와 Cost Factor를 통해 비밀번호 크래킹 비용을 증가시키지만 DB가 유출되면 공격자는 저장된 BCrypt 문자열에서 Version, Cost, Salt, Hash를 모두 얻을 수 있습니다. 따라서&amp;nbsp;약한&amp;nbsp;비밀번호에&amp;nbsp;대해서는&amp;nbsp;오프라인&amp;nbsp;딕셔너리&amp;nbsp;공격이&amp;nbsp;여전히&amp;nbsp;가능합니다. &lt;br /&gt;&lt;br /&gt;Pepper는&amp;nbsp;이런&amp;nbsp;상황에서&amp;nbsp;추가&amp;nbsp;방어층이&amp;nbsp;될&amp;nbsp;수&amp;nbsp;있습니다. &lt;br /&gt;Pepper를 DB와 분리된 Secret Manager, KMS, Vault, HSM 등에 안전하게 보관하면 공격자가&amp;nbsp;DB만&amp;nbsp;탈취했을&amp;nbsp;때&amp;nbsp;동일한&amp;nbsp;Hash를&amp;nbsp;재현하기&amp;nbsp;어려워집니다. &lt;br /&gt;&lt;br /&gt;다만&amp;nbsp;Pepper는&amp;nbsp;만능이&amp;nbsp;아닙니다. &lt;br /&gt;애플리케이션 서버, 환경 변수, Secret Manager, 배포 파이프라인까지 함께 노출되면 Pepper의 보호 효과는 크게 줄어들며&amp;nbsp;Pepper가&amp;nbsp;유출되면&amp;nbsp;기존&amp;nbsp;비밀번호&amp;nbsp;원문을&amp;nbsp;알&amp;nbsp;수&amp;nbsp;없기&amp;nbsp;때문에&amp;nbsp;교체와&amp;nbsp;마이그레이션이&amp;nbsp;어렵습니다. &lt;br /&gt;&lt;br /&gt;결국 BCrypt는 강력한 Password Hashing 알고리즘이지만 Salt,&amp;nbsp;Cost,&amp;nbsp;Pepper의&amp;nbsp;역할과&amp;nbsp;한계를&amp;nbsp;이해하고&amp;nbsp;위협&amp;nbsp;모델에&amp;nbsp;맞게&amp;nbsp;조합해야&amp;nbsp;합니다.&lt;/p&gt;</description>
      <category>Development/Spring</category>
      <author>KoreaNirsa</author>
      <guid isPermaLink="true">https://nirsa.tistory.com/489</guid>
      <comments>https://nirsa.tistory.com/489#entry489comment</comments>
      <pubDate>Thu, 4 Jun 2026 11:52:05 +0900</pubDate>
    </item>
    <item>
      <title>AGENTS.md와 이슈, 스펙, CodeRabbit으로 만드는 AI 개발 사이클</title>
      <link>https://nirsa.tistory.com/488</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AGENTS.md와&amp;nbsp;이슈,&amp;nbsp;스펙,&amp;nbsp;CodeRabbit으로&amp;nbsp;만드는&amp;nbsp;AI&amp;nbsp;개발&amp;nbsp;사이클&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 AI에게 단순히 &amp;ldquo;이 기능 구현해줘&amp;rdquo;라고 요청하지 않습니다. &lt;br /&gt;먼저 Issue를 만들고, 필요한 경우 Spec을 작성한 뒤, AGENTS.md와 작업 프롬프트를 기준으로 개발 흐름을 고정합니다. &lt;br /&gt;&lt;br /&gt;현재 제가 사용하는 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이슈 생성&lt;/li&gt;
&lt;li&gt;스펙 생성 또는 확인&lt;/li&gt;
&lt;li&gt;구현&lt;/li&gt;
&lt;li&gt;테스트 및 정적 검사&lt;/li&gt;
&lt;li&gt;PR 생성&lt;/li&gt;
&lt;li&gt;CodeRabbit 리뷰 요청&lt;/li&gt;
&lt;li&gt;리뷰 타당성 검토&lt;/li&gt;
&lt;li&gt;필요한 내용만 반영&lt;/li&gt;
&lt;li&gt;CodeRabbit 재리뷰&lt;/li&gt;
&lt;li&gt;최종 diff 검토&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 AI가 임의로 정한 방식이 아닙니다. &lt;br /&gt;프로젝트 루트에 있는 AGENTS.md, 제가 사용하는 작업 프롬프트, Issue, Spec, CodeRabbit이 함께 작동하면서 만들어지는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;왜 이슈를 먼저 만드는가&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;저는 작업을 시작할 때 가능한 한 Issue를 먼저 만듭니다. &lt;br /&gt;&lt;br /&gt;이슈를 사용하는 이유는 단순히 할 일을 기록하기 위해서가 아닙니다. 제가 작업할 때 이슈는&amp;nbsp;작업의&amp;nbsp;목적,&amp;nbsp;범위,&amp;nbsp;완료&amp;nbsp;기준을&amp;nbsp;고정하는&amp;nbsp;기준점입니다. &lt;br /&gt;&lt;br /&gt;AI와 함께 개발할 때 가장 위험한 부분은 구현 속도가 아니라 작업 범위가 흐려지는 것입니다. &lt;br /&gt;요청이 모호하면 AI는 그럴듯한 방향으로 기능을 넓히거나, Spec에 없는 동작을 추가할 수 있는데, 이슈는 그 위험을 줄이는 역할을 합니다. &lt;br /&gt;&lt;br /&gt;보통&amp;nbsp;이슈에는&amp;nbsp;다음&amp;nbsp;내용을&amp;nbsp;적습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해결하려는 문제&lt;/li&gt;
&lt;li&gt;현재 동작&lt;/li&gt;
&lt;li&gt;기대 동작&lt;/li&gt;
&lt;li&gt;변경 대상 범위&lt;/li&gt;
&lt;li&gt;제외할 범위&lt;/li&gt;
&lt;li&gt;검증 방법&lt;/li&gt;
&lt;li&gt;관련 스펙 또는 문서&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게&amp;nbsp;이슈를&amp;nbsp;만들어두면&amp;nbsp;작업&amp;nbsp;중&amp;nbsp;판단&amp;nbsp;기준이&amp;nbsp;명확해집니다. &lt;br /&gt;&lt;br /&gt;예를 들어 CodeRabbit이 좋은 제안을 하더라도, 그 제안이 현재 이슈 범위 밖이라면 반영하지 않습니다. 하지만 반대로 이슈의 목적과 맞고, 스펙에도 부합한다면 반영 대상이 됩니다. &lt;br /&gt;&lt;br /&gt;즉,&amp;nbsp;이슈는&amp;nbsp;다음&amp;nbsp;역할을&amp;nbsp;합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 범위를 고정합니다.&lt;/li&gt;
&lt;li&gt;AI의 추측 구현을 줄입니다.&lt;/li&gt;
&lt;li&gt;PR의 목적을 명확하게 만듭니다.&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;저에게 이슈는 작업 요청서이자 PR의 기준 문서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;언제 스펙을 만드는가&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 작업에 새로운 Spec이 필요한 것은 아닙니다. &lt;br /&gt;이미 OpenAPI, YAML Markdown 문서, 기존 도메인 문서가 있다면 해당 스펙을 기준으로 구현하면 됩니다. &lt;br /&gt;&lt;br /&gt;하지만 다음과 같은 경우에는 스펙을 먼저 만들거나 수정합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 API를 추가할 때&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;li&gt;이슈만으로 구현 기준이 부족할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스펙은 구현보다 먼저 있어야 합니다. &lt;br /&gt;구현을 먼저 하고 나중에 스펙을 맞추면, 실제로는 코드가 기준이 되기 때문에 스펙 기반 개발이 아니라 구현 기반 문서화가 됩니다. &lt;br /&gt;&lt;br /&gt;제가&amp;nbsp;원하는&amp;nbsp;흐름은&amp;nbsp;반대입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스펙&lt;/li&gt;
&lt;li&gt;구현&lt;/li&gt;
&lt;li&gt;검증&lt;/li&gt;
&lt;li&gt;리뷰&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&amp;nbsp;스펙을&amp;nbsp;정하고,&amp;nbsp;그&amp;nbsp;스펙에&amp;nbsp;맞춰&amp;nbsp;구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;&amp;nbsp;AGENTS.md란 무엇인가&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AGENTS.md는 AI 에이전트에게 이 프로젝트에서 어떻게 작업해야 하는지 알려주는 지침 파일입니다. &lt;br /&gt;&lt;br /&gt;사람 개발자로 비유하면 팀 컨벤션 문서, 개발 가이드, PR 규칙, 리뷰 기준을 하나로 묶은 문서에 가깝습니다. &lt;br /&gt;&lt;br /&gt;현재 제 프로젝트에서 AGENTS.md는 다음 역할을 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 언어로 답변해야 하는지 정합니다.&lt;/li&gt;
&lt;li&gt;구현 전에 무엇을 읽어야 하는지 정합니다.&lt;/li&gt;
&lt;li&gt;스펙을 기준으로만 구현하도록 제한합니다.&lt;/li&gt;
&lt;li&gt;계층별 책임을 정합니다.&lt;/li&gt;
&lt;li&gt;CodeRabbit 리뷰 요청 방식을 정합니다.&lt;/li&gt;
&lt;li&gt;리뷰 제안을 어떻게 판단할지 정합니다.&lt;/li&gt;
&lt;li&gt;테스트 실행 순서를 정합니다.&lt;/li&gt;
&lt;li&gt;최종 응답 형식을 고정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, AGENTS.md는 AI가 작업 중 마음대로 판단하지 않도록 작업 경계를 만들어주는 문서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;현재 사용하는 AGENTS.md의 핵심 내용&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 AGENTS.md의 첫 번째 원칙은 스펙 기반 구현입니다.&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;이 원칙 때문에 AI는 이슈나 스펙에 없는 기능을 임의로 추가하지 않습니다.&lt;/li&gt;
&lt;li&gt;요구사항이 불명확하면 추측해서 구현하지 않고, 질문하거나 TODO를 남기는 방향으로 동작합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어 규칙도 정해져 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채팅 응답은 기본적으로 한국어로 작성합니다.&lt;/li&gt;
&lt;li&gt;프로젝트의 commit, issue, PR 문구는 영어로 작성합니다.&lt;/li&gt;
&lt;li&gt;코드,&amp;nbsp;명령어,&amp;nbsp;식별자,&amp;nbsp;파일&amp;nbsp;경로&amp;nbsp;등&amp;nbsp;필요한&amp;nbsp;경우에만&amp;nbsp;영어를&amp;nbsp;사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적인&amp;nbsp;우선순위는&amp;nbsp;다음과&amp;nbsp;같습니다. &lt;br /&gt;&lt;br /&gt;1. System Prompt&lt;br /&gt;2. Developer Prompt&lt;br /&gt;3. App / Workspace Context&lt;br /&gt;4. AGENTS.md&lt;br /&gt;5. User Prompt&lt;br /&gt;6. Issue, PR, diff&lt;br /&gt;7. Spec 문서 &lt;br /&gt;8.&amp;nbsp;관련&amp;nbsp;코드 &lt;br /&gt;9.&amp;nbsp;테스트와&amp;nbsp;실행&amp;nbsp;결과 &lt;br /&gt;10. 최종 diff&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 AGENTS.md가 실제 구현 코드보다 먼저 기준으로 적용된다는 것입니다. &lt;br /&gt;&lt;br /&gt;AI가&amp;nbsp;바로&amp;nbsp;코드를&amp;nbsp;열고&amp;nbsp;수정하는&amp;nbsp;것이&amp;nbsp;아니라,&amp;nbsp;먼저&amp;nbsp;&amp;ldquo;이&amp;nbsp;프로젝트에서는&amp;nbsp;어떻게&amp;nbsp;작업해야&amp;nbsp;하는가&amp;rdquo;를&amp;nbsp;확인한&amp;nbsp;뒤에&amp;nbsp;이슈,&amp;nbsp;스펙,&amp;nbsp;코드&amp;nbsp;순서로&amp;nbsp;내려갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;AGENTS.md를 먼저 읽는 이유&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AGENTS.md를 먼저 읽는 이유는 작업 방식 자체를 고정하기 위해서입니다. &lt;br /&gt;&lt;br /&gt;현재 AGENTS.md에는 다음과 같은 규칙이 들어 있습니다.&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;프로젝트의 commit, issue, PR은 영어로 작성합니다.&lt;/li&gt;
&lt;li&gt;구현 전에 이슈와 스펙을 읽습니다.&lt;/li&gt;
&lt;li&gt;스펙에 없는 기능은 구현하지 않습니다.&lt;/li&gt;
&lt;li&gt;관련 없는 리팩터링을 하지 않습니다.&lt;/li&gt;
&lt;li&gt;CodeRabbit 제안은 무조건 반영하지 않습니다.&lt;/li&gt;
&lt;li&gt;테스트&amp;nbsp;결과를&amp;nbsp;최종&amp;nbsp;응답에&amp;nbsp;포함합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 규칙을 먼저 읽어야 이후 작업이 흔들리지 않습니다. &lt;br /&gt;&lt;br /&gt;만약 AI가 AGENTS.md보다 코드를 먼저 읽는다면, 기존 코드만 보고 임의로 구조를 바꾸거나 스펙에 없는 개선을 추가할 수 있습니다. &lt;br /&gt;하지만 AGENTS.md가 먼저 적용되면 &amp;ldquo;스펙 밖 구현 금지&amp;rdquo;, &amp;ldquo;최소 변경&amp;rdquo;, &amp;ldquo;관련 없는 리팩터링 금지&amp;rdquo; 같은 제한이 먼저 걸립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;실제 작업 순서&lt;/b&gt;&lt;/blockquote&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;AGENTS.md&lt;/li&gt;
&lt;li&gt;User Prompt&lt;/li&gt;
&lt;li&gt;Issue&lt;/li&gt;
&lt;li&gt;PR&lt;/li&gt;
&lt;li&gt;&amp;nbsp;diff&lt;/li&gt;
&lt;li&gt;Spec&lt;/li&gt;
&lt;li&gt;관련 코드&lt;/li&gt;
&lt;li&gt;테스트 결과&lt;/li&gt;
&lt;li&gt;최종 diff&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&amp;nbsp;프로젝트&amp;nbsp;규칙을&amp;nbsp;확인합니다. &lt;br /&gt;여기서 언어 규칙, 작업 순서, 테스트 방식, CodeRabbit 처리 방식, 최종 응답 형식을 확인합니다. &lt;br /&gt;&lt;br /&gt;그다음&amp;nbsp;사용자의&amp;nbsp;요청을&amp;nbsp;확인합니다. &lt;br /&gt;예를&amp;nbsp;들어&amp;nbsp;사용자가&amp;nbsp;&amp;ldquo;이슈&amp;nbsp;10번&amp;nbsp;구현해줘&amp;rdquo;라고&amp;nbsp;하면,&amp;nbsp;AI는&amp;nbsp;이&amp;nbsp;요청을&amp;nbsp;작업&amp;nbsp;목표로&amp;nbsp;잡습니다. &lt;br /&gt;&lt;br /&gt;그다음&amp;nbsp;관련&amp;nbsp;이슈와&amp;nbsp;PR&amp;nbsp;정보를&amp;nbsp;읽습니다. &lt;br /&gt;이&amp;nbsp;단계에서는&amp;nbsp;작업&amp;nbsp;목적과&amp;nbsp;범위를&amp;nbsp;확인합니다. &lt;br /&gt;&lt;br /&gt;그다음&amp;nbsp;스펙&amp;nbsp;문서를&amp;nbsp;읽습니다. &lt;br /&gt;API경로, 요청, 응답, 검증 조건, 예외 규칙을 확인합니다. &lt;br /&gt;&lt;br /&gt;그다음&amp;nbsp;관련&amp;nbsp;코드를&amp;nbsp;읽습니다. &lt;br /&gt;하지만&amp;nbsp;이때도&amp;nbsp;코드를&amp;nbsp;보고&amp;nbsp;마음대로&amp;nbsp;바꾸는&amp;nbsp;것이&amp;nbsp;아니라,&amp;nbsp;앞에서&amp;nbsp;읽은&amp;nbsp;이슈와&amp;nbsp;스펙에&amp;nbsp;필요한&amp;nbsp;부분만&amp;nbsp;수정합니다. &lt;br /&gt;&lt;br /&gt;마지막으로 테스트와 diff를 확인합니다. &lt;br /&gt;여기서&amp;nbsp;스펙과&amp;nbsp;구현이&amp;nbsp;맞는지,&amp;nbsp;변경&amp;nbsp;범위가&amp;nbsp;과하지&amp;nbsp;않은지,&amp;nbsp;기존&amp;nbsp;계약을&amp;nbsp;깨지&amp;nbsp;않았는지&amp;nbsp;다시&amp;nbsp;확인합니다. &lt;br /&gt;&lt;br /&gt;계층별&amp;nbsp;책임도&amp;nbsp;프롬프트로&amp;nbsp;제한합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;AGENTS.md에는 코드 계층별 규칙도 들어 있습니다. &lt;br /&gt;&lt;br /&gt;Controller&lt;br /&gt;-&amp;nbsp;비즈니스&amp;nbsp;로직을&amp;nbsp;포함하지&amp;nbsp;않습니다. &lt;br /&gt;-&amp;nbsp;요청과&amp;nbsp;응답&amp;nbsp;매핑만&amp;nbsp;담당합니다. &lt;br /&gt;&lt;br /&gt;Service&lt;br /&gt;-&amp;nbsp;비즈니스&amp;nbsp;로직은&amp;nbsp;서비스&amp;nbsp;계층에&amp;nbsp;둡니다. &lt;br /&gt;- transaction을 명시적으로 설정합니다. &lt;br /&gt;-&amp;nbsp;비즈니스&amp;nbsp;검증을&amp;nbsp;서비스&amp;nbsp;계층에서&amp;nbsp;수행합니다. &lt;br /&gt;&lt;br /&gt;DTO&lt;br /&gt;-&amp;nbsp;요청&amp;nbsp;DTO와&amp;nbsp;응답&amp;nbsp;DTO를&amp;nbsp;분리합니다. &lt;br /&gt;- 검증 annotation을 DTO에 둡니다. &lt;br /&gt;-&amp;nbsp;필요한&amp;nbsp;경우&amp;nbsp;변환&amp;nbsp;로직을&amp;nbsp;DTO&amp;nbsp;내부에&amp;nbsp;둡니다. &lt;br /&gt;&lt;br /&gt;Entity&lt;br /&gt;-&amp;nbsp;상태와&amp;nbsp;도메인&amp;nbsp;동작만&amp;nbsp;유지합니다. &lt;br /&gt;- 생성을 위해 static factory method를 선호합니다. &lt;br /&gt;&lt;br /&gt;Repository&lt;br /&gt;-&amp;nbsp;데이터&amp;nbsp;접근만&amp;nbsp;담당합니다. &lt;br /&gt;이 규칙은 AI가 Controller에 비즈니스 로직을 넣거나, DTO와 Entity의 책임을 섞는 일을 줄여줍니다. &lt;br /&gt;결과적으로&amp;nbsp;코드&amp;nbsp;구조가&amp;nbsp;기존&amp;nbsp;프로젝트&amp;nbsp;방식에서&amp;nbsp;벗어나지&amp;nbsp;않게&amp;nbsp;됩니다. &lt;br /&gt;&lt;br /&gt;금지&amp;nbsp;사항도&amp;nbsp;명확히&amp;nbsp;둡니다 &lt;br /&gt;AI에게&amp;nbsp;가장&amp;nbsp;위험한&amp;nbsp;요청은&amp;nbsp;&amp;ldquo;좋게&amp;nbsp;알아서&amp;nbsp;해줘&amp;rdquo;입니다. &lt;br /&gt;그래서&amp;nbsp;현재&amp;nbsp;지침에는&amp;nbsp;금지&amp;nbsp;사항도&amp;nbsp;명확히&amp;nbsp;적어두었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스펙에 없는 기능을 추가하지 않습니다.&lt;/li&gt;
&lt;li&gt;필드를 임의로 만들지 않습니다.&lt;/li&gt;
&lt;li&gt;전체 구조를 다시 작성하지 않습니다.&lt;/li&gt;
&lt;li&gt;관련 없는 코드를 수정하지 않습니다.&lt;/li&gt;
&lt;li&gt;기존 API계약을 깨지 않습니다.&lt;/li&gt;
&lt;li&gt;검증되지 않은 로직을 PR에 포함하지 않습니다.&lt;/li&gt;
&lt;li&gt;PR 목적 밖의 리팩터링을 하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 규칙은 변경 범위를 줄이는 데 큰 역할을 합니다. &lt;br /&gt;AI가&amp;nbsp;코드를&amp;nbsp;더&amp;nbsp;깔끔하게&amp;nbsp;만들고&amp;nbsp;싶어도,&amp;nbsp;현재&amp;nbsp;이슈와&amp;nbsp;관계없으면&amp;nbsp;수정하지&amp;nbsp;않도록&amp;nbsp;제한합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;CodeRabbit 리뷰 흐름&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 사이클의 핵심 중 하나는 CodeRabbit 리뷰입니다. &lt;br /&gt;AGENTS.md에는 Draft PR 또는 리뷰 후속 작업에서는 CodeRabbit 리뷰를 먼저 요청하도록 되어 있습니다. &lt;br /&gt;&lt;br /&gt;첫 리뷰는 전체 리뷰로 요청합니다. 아래 명령은 해당 PR에 CodeRabbit 전체 리뷰를 요청하는 명령입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779606861616&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gh pr comment &amp;lt;PR_NUMBER&amp;gt; --repo KoreaNirsa/spec-guard --body &quot;@coderabbitai full review&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후속 리뷰는 증분 리뷰로 요청합니다. 아래 명령은 새로 추가된 변경 사항 중심으로 CodeRabbit 리뷰를 요청하는 명령입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779606971829&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gh pr comment &amp;lt;PR_NUMBER&amp;gt; --repo KoreaNirsa/spec-guard --body &quot;@coderabbitai review&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게&amp;nbsp;나누는&amp;nbsp;이유는&amp;nbsp;명확합니다. &lt;br /&gt;첫 리뷰에서는 PR 전체를 확인한 후&amp;nbsp;새로&amp;nbsp;반영한&amp;nbsp;변경만&amp;nbsp;확인하는&amp;nbsp;편이&amp;nbsp;더&amp;nbsp;효율적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;CodeRabbit YAML을 사용하지 않은 이유&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CodeRabbit은 별도의 YAML을 통해 리뷰 규칙을 지정할 수 있습니다. &lt;br /&gt;하지만 현재 프로젝트에서는 CodeRabbit YAML을 중심으로 흐름을 만들지 않고 AGENTS.md, 이슈, 스펙, PR 흐름을 기준으로 작업 방식을 고정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 번째 이유&lt;/b&gt;는 이 프로젝트가 OSS 프로젝트이기 때문입니다.&lt;br /&gt;OSS 프로젝트에서는 특정 도구의 설정 파일보다, 사람이 읽고 이해할 수 있는 작업 원칙이 중요하다고 생각합니다. &lt;br /&gt;외부 기여자나 리뷰어가 저장소를 봤을 때 &amp;ldquo;이 프로젝트는 어떤 기준으로 작업하는가&amp;rdquo;를 쉽게 이해할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 번째 이유&lt;/b&gt;는 CodeRabbit YAML이 CodeRabbit의 리뷰 동작에는 영향을 줄 수 있지만, AI 구현 흐름 전체를 통제하지는 못하기 때문입니다. &lt;br /&gt;&lt;br /&gt;현재&amp;nbsp;제가&amp;nbsp;원하는&amp;nbsp;흐름은&amp;nbsp;단순&amp;nbsp;리뷰&amp;nbsp;설정이&amp;nbsp;아닙니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이슈 생성&lt;/li&gt;
&lt;li&gt;스펙 확인&lt;/li&gt;
&lt;li&gt;구현&lt;/li&gt;
&lt;li&gt;테스트&lt;/li&gt;
&lt;li&gt;PR&lt;/li&gt;
&lt;li&gt;CodeRabbit 리뷰&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;이 전체 흐름은 CodeRabbit만으로는 만들 수 없습니다. &lt;br /&gt;CodeRabbit은 PR이 만들어진 뒤 리뷰를 도와주는 도구에 가깝습니다. &lt;br /&gt;반면 AGENTS.md는 구현 전 단계부터 AI가 무엇을 읽고, 무엇을 기준으로 판단하고, 어떤 범위 안에서 수정해야 하는지를 정합니다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;세&amp;nbsp;번째&amp;nbsp;이유&lt;/b&gt;는 특정 리뷰 도구에 작업 기준이 종속되는 것을 피하기 위해서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서 더 중요한 기준은 CodeRabbit 아니라 이슈와 스펙입니다. &lt;br /&gt;&lt;br /&gt;현재&amp;nbsp;구조에서는&amp;nbsp;역할을&amp;nbsp;이렇게&amp;nbsp;나눕니다. &lt;br /&gt;&lt;br /&gt;1. Issue&lt;br /&gt;&amp;nbsp; &amp;rarr; 작업 목적과 범위를 정합니다.&lt;br /&gt;2. Spec&lt;br /&gt;&amp;nbsp; &amp;rarr; 구현 기준과 API 계약을 정합니다.&lt;br /&gt;3. AGENTS.md&lt;br /&gt;&amp;nbsp; &amp;rarr; AI가 따라야 할 작업 방식과 제한을 정합니다.&lt;br /&gt;4. CodeRabbit&lt;br /&gt;&amp;nbsp; &amp;rarr; PR 이후 보조 리뷰를 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 분리하면 CodeRabbit은 중요한 보조 도구로 사용할 수 있지만, 프로젝트의 최종 기준이 되지는 않습니다. &lt;br /&gt;&lt;br /&gt;CodeRabbit 제안은 무조건 반영하지 않습니다. 중요한 점은 CodeRabbit의 모든 제안을 적용하지 않는다는 것입니다. &lt;br /&gt;&lt;br /&gt;현재&amp;nbsp;지침에서는&amp;nbsp;다음&amp;nbsp;조건을&amp;nbsp;만족하는&amp;nbsp;제안만&amp;nbsp;반영하도록&amp;nbsp;되어&amp;nbsp;있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;PR목적과 일치합니다.&lt;/li&gt;
&lt;li&gt;관련 이슈와 일치합니다.&lt;/li&gt;
&lt;li&gt;스펙 또는 API 계약과 일치합니다.&lt;/li&gt;
&lt;li&gt;diff를 최소로 유지합니다.&lt;/li&gt;
&lt;li&gt;테스트로&amp;nbsp;검증할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;반대로 다음 제안은 반영하지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관련 없는 리팩터링입니다.&lt;/li&gt;
&lt;li&gt;스펙에 없는 기능입니다.&lt;/li&gt;
&lt;li&gt;큰 구조 변경입니다.&lt;/li&gt;
&lt;li&gt;단순 스타일 취향입니다.&lt;/li&gt;
&lt;li&gt;API 계약에 위험합니다.&lt;/li&gt;
&lt;li&gt;현재 PR 범위 밖입니다.&lt;/li&gt;
&lt;li&gt;근거가&amp;nbsp;약하거나&amp;nbsp;모호합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;즉, CodeRabbit은 최종 결정자가 아니라 보조 리뷰어입니다. 최종 판단 기준은 항상 이슈, 스펙, API 계약입니다. &lt;br /&gt;&lt;br /&gt;테스트&amp;nbsp;순서도&amp;nbsp;정해둡니다 &lt;br /&gt;변경&amp;nbsp;후에는&amp;nbsp;테스트를&amp;nbsp;실행합니다. &lt;br /&gt;이때도&amp;nbsp;무작정&amp;nbsp;전체&amp;nbsp;테스트부터&amp;nbsp;돌리지&amp;nbsp;않고,&amp;nbsp;지침에&amp;nbsp;따라&amp;nbsp;순서를&amp;nbsp;둡니다. &lt;br /&gt;&lt;br /&gt;1.&amp;nbsp;변경&amp;nbsp;파일&amp;nbsp;대상&amp;nbsp;테스트 &lt;br /&gt;2.&amp;nbsp;영향받은&amp;nbsp;도메인의&amp;nbsp;기존&amp;nbsp;테스트 &lt;br /&gt;3. 이슈나 PR에 명시된 검증 명령 &lt;br /&gt;4. 필요할 경우 전체 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를&amp;nbsp;실행할&amp;nbsp;수&amp;nbsp;없는&amp;nbsp;경우에도&amp;nbsp;그냥&amp;nbsp;넘어가지&amp;nbsp;않습니다. &lt;br /&gt;실행하지&amp;nbsp;못한&amp;nbsp;명령,&amp;nbsp;이유,&amp;nbsp;남은&amp;nbsp;위험을&amp;nbsp;최종&amp;nbsp;응답에&amp;nbsp;남기도록&amp;nbsp;되어&amp;nbsp;있습니다. &lt;br /&gt;&lt;br /&gt;최종&amp;nbsp;리뷰도&amp;nbsp;AI가&amp;nbsp;다시&amp;nbsp;수행합니다 &lt;br /&gt;작업이 끝나면 최종 diff를 다시 확인합니다. &lt;br /&gt;&lt;br /&gt;확인 기준은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이슈와 스펙을 다시 확인했는지&lt;/li&gt;
&lt;li&gt;diff가 PR 범위 안에 있는지&lt;/li&gt;
&lt;li&gt;스펙에 없는 기능이 추가되지 않았는지&lt;/li&gt;
&lt;li&gt;API 계약이 깨지지 않았는지&lt;/li&gt;
&lt;li&gt;테스트가 충분한지&lt;/li&gt;
&lt;li&gt;회귀 위험이 허용 가능한지&lt;/li&gt;
&lt;li&gt;관련 없는 리팩터링이나 포맷 변경이 없는지&lt;/li&gt;
&lt;li&gt;CodeRabbit 제안을 과하게 반영하지 않았는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 단계는 사람이 PR을 리뷰하는 관점과 비슷합니다. &lt;br /&gt;단순히 &amp;ldquo;구현이 끝났다&amp;rdquo;가 아니라 &amp;ldquo;PR로 올릴 수 있는 상태인가&amp;rdquo;를 다시 확인하는 과정입니다. &lt;br /&gt;&lt;br /&gt;최종&amp;nbsp;응답&amp;nbsp;형식도&amp;nbsp;고정합니다 &lt;br /&gt;현재 AGENTS.md는 최종 응답 형식까지 지정합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Changed Files(변경 파일)&lt;/li&gt;
&lt;li&gt;Reason(변경 이유)&lt;/li&gt;
&lt;li&gt;Code(코드 변경 내용)&lt;/li&gt;
&lt;li&gt;CodeRabbit Results(코드래빗 결과)&lt;/li&gt;
&lt;li&gt;Codex Final Review(코덱스 최종 검토)&lt;/li&gt;
&lt;li&gt;Tests(테스트)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 형식을 고정해두면 작업 결과를 매번 같은 기준으로 확인할 수 있습니다. &lt;br /&gt;무엇이&amp;nbsp;바뀌었는지,&amp;nbsp;왜&amp;nbsp;바뀌었는지,&amp;nbsp;어떤&amp;nbsp;테스트를&amp;nbsp;했는지,&amp;nbsp;남은&amp;nbsp;위험은&amp;nbsp;무엇인지&amp;nbsp;빠르게&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;전체 흐름이 동작하는 방식&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 흐름은 하나의 프롬프트만으로 만들어지는 것이 아닙니다. &lt;br /&gt;&lt;br /&gt;크게&amp;nbsp;보면&amp;nbsp;다음&amp;nbsp;요소들이&amp;nbsp;함께&amp;nbsp;작동합니다. &lt;br /&gt;1. Codex 기본 개발 에이전트 동작 &lt;br /&gt;2. 프로젝트 루트의 AGENTS.md&lt;br /&gt;3. Issue&lt;br /&gt;4. Spec&lt;br /&gt;5. GitHub / CodeRabbit 관련 작업 프롬프트 &lt;br /&gt;6. 프로젝트 규칙&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex 기본 동작은 파일을 읽고, 코드를 수정하고, 테스트를 실행하고, diff를 확인하는 기반이 됩니다. &lt;br /&gt;&lt;br /&gt;그 위에 AGENTS.md가 프로젝트 전용 규칙을 덮어씌웁니다. &lt;br /&gt;예를 들어 한국어 응답,&amp;nbsp; commit / PR 규칙, 스펙 기반 구현, CodeRabbit 리뷰 사이클 같은 내용이 여기서 정해집니다. &lt;br /&gt;&lt;br /&gt;Issue는 작업 목적과 범위를 고정합니다. &lt;br /&gt;Spec은 구현 기준을 고정합니다. &lt;br /&gt;CodeRabbit은 PR 이후 보조 리뷰어 역할을 합니다. &lt;br /&gt;&lt;br /&gt;정리하면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Codex 기본 동작 &lt;br /&gt;&amp;nbsp; &amp;rarr; 개발 작업을 수행하는 기본 능력입니다.&lt;br /&gt;2. AGENTS.md&lt;br /&gt;&amp;nbsp; &amp;rarr; 이 프로젝트에서 반드시 따라야 하는 작업 규칙입니다.&lt;br /&gt;3. Issue&lt;br /&gt;&amp;nbsp; &amp;rarr; 작업 목적과 범위를 고정합니다.&lt;br /&gt;4. Spec&lt;br /&gt;&amp;nbsp; &amp;rarr; 구현 기준과 API 계약을 정합니다.&lt;br /&gt;5. CodeRabbit&lt;br /&gt;&amp;nbsp; &amp;rarr; PR 이후 보조 리뷰를 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실제 개발 사이클&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;설정을&amp;nbsp;적용하면&amp;nbsp;실제&amp;nbsp;작업은&amp;nbsp;다음처럼&amp;nbsp;진행됩니다. &lt;br /&gt;&lt;br /&gt;1.&amp;nbsp;이슈를&amp;nbsp;만듭니다. &lt;br /&gt;2.&amp;nbsp;이슈에&amp;nbsp;문제,&amp;nbsp;기대&amp;nbsp;동작,&amp;nbsp;범위,&amp;nbsp;검증&amp;nbsp;방법을&amp;nbsp;적습니다. &lt;br /&gt;3.&amp;nbsp;필요한&amp;nbsp;경우&amp;nbsp;스펙을&amp;nbsp;새로&amp;nbsp;만들거나&amp;nbsp;기존&amp;nbsp;스펙을&amp;nbsp;수정합니다. &lt;br /&gt;4. AGENTS.md의 작업 규칙을 기준으로 삼습니다. &lt;br /&gt;5.&amp;nbsp;이슈와&amp;nbsp;스펙을&amp;nbsp;읽습니다. &lt;br /&gt;6.&amp;nbsp;구현해야&amp;nbsp;할&amp;nbsp;입력과&amp;nbsp;출력을&amp;nbsp;정리합니다. &lt;br /&gt;7. Controller, Service, DTO, Repository 중 어떤 계층이 영향을 받는지 확인합니다. &lt;br /&gt;8.&amp;nbsp;스펙에&amp;nbsp;필요한&amp;nbsp;코드만&amp;nbsp;수정합니다. &lt;br /&gt;9.&amp;nbsp;테스트와&amp;nbsp;정적&amp;nbsp;검사를&amp;nbsp;실행합니다. &lt;br /&gt;10. diff를 확인합니다. &lt;br /&gt;11. 브랜치를 만들고 commit합니다. &lt;br /&gt;12. PR을 생성합니다. &lt;br /&gt;13. CodeRabbit 전체 리뷰를 요청합니다. &lt;br /&gt;14. CodeRabbit 의견을 확인합니다. &lt;br /&gt;15.&amp;nbsp;타당한&amp;nbsp;의견만&amp;nbsp;반영합니다. &lt;br /&gt;16.&amp;nbsp;다시&amp;nbsp;테스트합니다. &lt;br /&gt;17. 추가 commit을 push합니다. &lt;br /&gt;18. CodeRabbit 증분 리뷰를 요청합니다. &lt;br /&gt;19.&amp;nbsp;남은&amp;nbsp;의견을&amp;nbsp;다시&amp;nbsp;검토합니다. &lt;br /&gt;20. 최종 diff와 위험을 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 흐름의 핵심은 반복입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이슈&lt;/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;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;&lt;br /&gt;리뷰가 있다고 해서 무조건 반영하지 않고, 반영했다고 해서 바로 끝내지 않습니다. 다시&amp;nbsp;검증하고&amp;nbsp;다시&amp;nbsp;리뷰합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 방식의 장점&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사이클을 사용하면 다음 장점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스펙 밖 구현을 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;PR 범위가 작고 명확하게 유지됩니다.&lt;/li&gt;
&lt;li&gt;CodeRabbit 제안을 무비판적으로 반영하지 않게 됩니다.&lt;/li&gt;
&lt;li&gt;API 계약을 깨뜨릴 위험을 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;최종 응답에서 변경 이유와 테스트 결과를 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;사람 리뷰어가 PR 의도를 이해하기 쉬워집니다.&lt;/li&gt;
&lt;li&gt;나중에 이슈와 PR을 통해 변경 이유를 추적할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;특히&amp;nbsp;AI와&amp;nbsp;함께&amp;nbsp;개발할&amp;nbsp;때&amp;nbsp;중요한&amp;nbsp;것은&amp;nbsp;속도보다&amp;nbsp;통제입니다. &lt;br /&gt;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;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정리&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 작업 사이클은 단순 자동화가 아닙니다. &lt;br /&gt;AGENTS.md와 프롬프트를 통해 AI의 작업 방식을 프로젝트에 맞게 제한하고, Issue와 Spec으로 작업 기준을 고정하며, CodeRabbit을 보조 리뷰어로 활용하는 구조입니다. &lt;br /&gt;&lt;br /&gt;AI에게 코드를 맡기는 것이 아니라 AI가 따라야 할 작업 기준을 먼저 정하고, 그&amp;nbsp;기준&amp;nbsp;안에서&amp;nbsp;구현과&amp;nbsp;리뷰를&amp;nbsp;반복하게&amp;nbsp;만드는&amp;nbsp;방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이슈를 통해 작업 목적과 범위를 고정하고 스펙을 통해 구현 기준을 명확히 합니다. &lt;br /&gt;AGENTS.md를 통해 AI가 따라야 할 작업 규칙을 정합니다. &lt;br /&gt;CodeRabbit을 통해 보조 리뷰를 받고, 그 제안을 다시 이슈와 스펙 기준으로 검토합니다. &lt;br /&gt;&lt;br /&gt;결국 이 흐름은 다음 목표를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스펙 &amp;rarr; 구현 &amp;rarr; 검증 &amp;rarr; 리뷰&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;스펙을&amp;nbsp;먼저&amp;nbsp;읽고,&amp;nbsp;필요한&amp;nbsp;것만&amp;nbsp;구현하고,&amp;nbsp;테스트로&amp;nbsp;검증하고,&amp;nbsp;리뷰를&amp;nbsp;통해&amp;nbsp;다시&amp;nbsp;확인합니다. &lt;br /&gt;이 과정을 반복하면 AI를 사용하더라도 PR의 목적과 코드 품질을 더 안정적으로 유지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 이러한 워크플로우를 자동화 함으로써 이슈를 처리하면 코드 구현부터 리뷰, 재검증까지 한번의 싸이클로 가져갈 수 있습니다.&lt;/p&gt;</description>
      <category>AI Engineering/Methodology</category>
      <author>KoreaNirsa</author>
      <guid isPermaLink="true">https://nirsa.tistory.com/488</guid>
      <comments>https://nirsa.tistory.com/488#entry488comment</comments>
      <pubDate>Sun, 24 May 2026 16:45:33 +0900</pubDate>
    </item>
    <item>
      <title>SpecGuard 소개: AI 코딩 전에 스펙을 먼저 검사하는 도구</title>
      <link>https://nirsa.tistory.com/487</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard&amp;nbsp;소개:&amp;nbsp;AI&amp;nbsp;코딩&amp;nbsp;전에&amp;nbsp;스펙을&amp;nbsp;먼저&amp;nbsp;검사하는&amp;nbsp;도구&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최근 Codex, Claude Code 같은 AI 코딩 도구를 사용하면서 개발 방식이 많이 바뀌고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;간단한 기능은 설명 몇 줄만으로도 빠르게 구현되고, 익숙하지 않은 코드베이스에서도 에이전트가 생각보다 많은 작업을 처리해줍니다.&amp;nbsp;저&amp;nbsp;역시&amp;nbsp;이런&amp;nbsp;도구를&amp;nbsp;사용하면서&amp;nbsp;개발&amp;nbsp;속도가&amp;nbsp;빨라지는&amp;nbsp;경험을&amp;nbsp;했습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그런데 여러 번 사용해보니 문제가 단순히 &amp;ldquo;AI가 코드를 잘 짜느냐&amp;rdquo;에만 있지 않았습니다. 오히려 더 자주 부딪힌 문제는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;AI에게 넘긴 스펙 자체가 불완전하다&lt;/i&gt;&lt;/span&gt;는 점이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;문제는 코드가 아니라 입력물에 있었다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AI 코딩 도구는 주어진 입력을 바탕으로 구현을 진행합니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;입력이&amp;nbsp;명확하면&amp;nbsp;꽤&amp;nbsp;좋은&amp;nbsp;결과를&amp;nbsp;만들&amp;nbsp;수&amp;nbsp;있지만,&amp;nbsp;스펙이&amp;nbsp;애매하거나&amp;nbsp;빠진&amp;nbsp;부분이&amp;nbsp;많으면&amp;nbsp;AI는&amp;nbsp;그&amp;nbsp;빈틈을&amp;nbsp;나름대로&amp;nbsp;추측해서&amp;nbsp;채웁니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 구현이 그럴듯해 보입니다. 하지만 개발이 진행될수록 문제가 커집니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코드의 품질과 구조가 점점 무너집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;스펙과 코드가 점점 맞지 않게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;나중에는 코드가 틀린 건지, 스펙이 낡은 건지, 처음 요구사항이 애매했던 건지 구분하기 어려워집니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;특히&amp;nbsp;권한,&amp;nbsp;소유권,&amp;nbsp;상태&amp;nbsp;전이,&amp;nbsp;재시도,&amp;nbsp;멱등성,&amp;nbsp;API&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;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기존에는&amp;nbsp;이런&amp;nbsp;문제를&amp;nbsp;주로&amp;nbsp;구현&amp;nbsp;이후&amp;nbsp;코드&amp;nbsp;리뷰에서&amp;nbsp;잡으려고&amp;nbsp;했습니다.&amp;nbsp;하지만&amp;nbsp;AI&amp;nbsp;코딩에서는&amp;nbsp;구현&amp;nbsp;속도가&amp;nbsp;빠른&amp;nbsp;만큼,&amp;nbsp;잘못된&amp;nbsp;방향으로도&amp;nbsp;빠르게&amp;nbsp;진행됩니다.&amp;nbsp;그래서&amp;nbsp;저는&amp;nbsp;구현&amp;nbsp;이후&amp;nbsp;코드&amp;nbsp;리뷰만으로는&amp;nbsp;부족하다고&amp;nbsp;생각했습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결함 있는 스펙이 구현 에이전트에게 넘어가기 전에, 스펙 자체의 빈틈을 먼저 드러내는 단계가 필요했습니다. 이 문제를 줄이기 위해 만든 오픈소스 도구가 &lt;b&gt;SpecGuard&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;spec_guard_banner.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8jp2y/dJMcah5yjlt/WbF9I7hSF5AKzPpzHxkH00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8jp2y/dJMcah5yjlt/WbF9I7hSF5AKzPpzHxkH00/img.png&quot; data-alt=&quot;GitHub : https://github.com/KoreaNirsa/spec-guard&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8jp2y/dJMcah5yjlt/WbF9I7hSF5AKzPpzHxkH00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8jp2y%2FdJMcah5yjlt%2FWbF9I7hSF5AKzPpzHxkH00%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;1376&quot; height=&quot;768&quot; data-filename=&quot;spec_guard_banner.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GitHub : https://github.com/KoreaNirsa/spec-guard&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard는 무엇인가&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard는 AI에게 구현을 맡기기 전에 스펙을 먼저 검사하는 CLI / Codex 플러그인입니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코드 생성기가 아닙니다. 프롬프트를 넣으면 코드를 만들어주는 도구도 아닙니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard는 구현 전에 스펙 패키지를 검토하고, 이 스펙을 AI 구현 에이전트에게 넘겨도 되는지 판단하는 도구에 가깝습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기본&amp;nbsp;흐름은&amp;nbsp;다음과&amp;nbsp;같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 사람이 제품 스펙을 작성합니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.&amp;nbsp;SpecGuard가&amp;nbsp;스펙을&amp;nbsp;검사합니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. NOT_READY라면 스펙을 보강합니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. READY 또는 READY_WITH_WARNINGS가 되면 Codex, Claude Code 같은 구현 에이전트에게 넘깁니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5.&amp;nbsp;구현&amp;nbsp;후에는&amp;nbsp;선택적으로&amp;nbsp;PR&amp;nbsp;Review를&amp;nbsp;통해&amp;nbsp;코드가&amp;nbsp;승인된&amp;nbsp;스펙에서&amp;nbsp;벗어나지&amp;nbsp;않았는지&amp;nbsp;확인합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;저는 이 흐름을 &lt;b&gt;Validation-First Workflow(VFW)&lt;/b&gt;라고 부르고 있습니다. 핵심은 &amp;ldquo;AI에게 코드를 더 잘 짜게 하자&amp;rdquo;가 아니라, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;ldquo;&lt;/span&gt;AI에게 넘기기 전에 입력물을 먼저 검증하자&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rdquo;&lt;/span&gt; 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;A. 어떤 문제를 찾는가&lt;/b&gt; &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard는 주로 구현 전에 놓치기 쉬운 스펙의 빈틈을 찾습니다. 예를 들면 다음과 같은 것들입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인증/권한 경계가 불명확한 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;tenant/user ownership 범위가 빠진 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;idempotency, replay, race condition 처리가 없는 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;만료/철회/상태 전이 규칙이 애매한 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;webhook, background job, 외부 API 재시도 정책이 없는 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;API 계약은 있다고 했지만 실제 OpenAPI path가 비어 있는 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;클라이언트 검증만 믿고 서버 측 검증이 빠진 요구사항&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결과는 크게 세 가지로 나뉩니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;READY: 구현 에이전트에게 넘길 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;READY_WITH_WARNINGS: 넘길 수는 있지만 주의할 점이 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NOT_READY: Critical/Major 문제가 있어 스펙 보강 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NOT_READY가 나오면 SpecGuard는 구현을 시작하지 말고 스펙을 먼저 고치라고 알려줍니다. 이 점이 중요합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard의 목적은 구현을 막는 것이 아니라, &lt;b&gt;잘못된 구현이 시작되기 전에 멈출 수 있는 지점을 만드는 것&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;B. 기본 검사는 API 키 없이 동작한다&lt;/b&gt; &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard의 기본 검사는 로컬 휴리스틱 기반이며 OpenAI&amp;nbsp;API&amp;nbsp;Key&amp;nbsp;없이도&amp;nbsp;실행할&amp;nbsp;수&amp;nbsp;있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779032495519&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install spec-guard
specguard run specs/your-feature-name --no-llm --no-follow-up&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기본&amp;nbsp;경로를&amp;nbsp;LLM&amp;nbsp;기반&amp;nbsp;리뷰가&amp;nbsp;아니라&amp;nbsp;휴리스틱&amp;nbsp;검사로&amp;nbsp;둔&amp;nbsp;이유는&amp;nbsp;단순합니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CI나 PR Review에서는 결과가 빠르고 재현 가능한 것이 중요하기 때문입니다. LLM 기반 리뷰는 비용, 속도, 설정, 모델 변화의 영향을 받을 수 있기 때문에&amp;nbsp;현재&amp;nbsp;기본&amp;nbsp;경로는&amp;nbsp;빠른&amp;nbsp;로컬&amp;nbsp;검사입니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;더 깊은 리뷰가 필요할 때만 OpenAI Platform 또는 Codex 기반 상세 리뷰를 선택적으로 붙이는 방향으로 두고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;C.&amp;nbsp; SpecGuard가 만드는 결과물&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard가 스펙을 검사하면 사람이 읽을 수 있는 리뷰와 기계가 읽을 수 있는 JSON 결과를 함께 만들며 대표적으로 다음과 같은 결과물이 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;readiness-review.md: 사람이 읽는 스펙 준비 상태 리뷰&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;readiness-review.json: 플러그인이나 CI가 읽을 수 있는 구조화된 결과&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;implementation-output.md: 구현 에이전트에게 넘길 수 있는 핸드오프 문서&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 중요한 파일은 implementation-output.md입니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard가 스펙을 READY 또는 READY_WITH_WARNINGS로 판단하면, 구현 에이전트에게 넘길 입력을 정리한 핸드오프 문서를 만들 수 있습니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;즉, Codex나 Claude Code에게 바로 &amp;ldquo;알아서 구현해줘&amp;rdquo;라고 넘기는 대신, SpecGuard가 검토한 스펙과 구현 경계, 테스트/계약 정보를 바탕으로 구현을 시작하도록 만드는 흐름입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;D.&amp;nbsp; Codex 플러그인을 추가한 이유&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;v0.4.0 작업에서는 Codex 앱 플러그인 MVP를 추가했습니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기존에는 사용자가 CLI 명령어를 직접 알고 실행해야 했지만 Codex를 쓰는 사용자는 자연스럽게 이렇게 말하고 싶을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779032593009&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpecGuard specs/your-feature-name 스펙 패키지에 대해 SpecGuard를 실행하고 READY/NOT_READY 상태와 주요 finding을 요약해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard Codex 플러그인은 이 흐름을 돕기 위한 것이며 Codex 플러그인은 SpecGuard 엔진을 새로 구현하지 않습니다. 기존 specguard CLI를 호출하고, 생성된 구조화 결과를 읽어서 현재 상태와 다음 행동을 요약합니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설치&amp;nbsp;흐름은&amp;nbsp;대략&amp;nbsp;다음과&amp;nbsp;같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779032649508&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install spec-guard
specguard --help
codex plugin marketplace add KoreaNirsa/spec-guard --ref main&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이후 Codex 앱 플러그인에서 Bulit by OpenAI &amp;rarr; SpecGuard Plugins 소스를 선택하고 설치하면 됩니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다만 중요한 제한이 존재하는데, 플러그인 설치가 specguard CLI 설치까지 자동으로 처리하는 것은 아닙니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재 MVP는 CLI-backed 구조입니다. 따라서 Codex가 실행되는 환경에서 specguard 명령이 동작해야 합니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앞으로는 사용자가 직접 명령어를 몰라도 Codex에게 &amp;ldquo;SpecGuard CLI가 설치되어 있는지 확인하고, 없으면 설치한 뒤 실행해줘&amp;rdquo;라고 요청할 수 있는 흐름을 더 자연스럽게 만들어 가려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;E. PR Review도 지원한다&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard에는 GitHub Actions 기반 PR Review 흐름도 있습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;구현&amp;nbsp;PR이&amp;nbsp;올라왔을&amp;nbsp;때,&amp;nbsp;승인된&amp;nbsp;스펙&amp;nbsp;패키지와&amp;nbsp;구현&amp;nbsp;diff를&amp;nbsp;비교해서&amp;nbsp;advisory&amp;nbsp;comment를&amp;nbsp;남기는&amp;nbsp;방식입니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 목표는 &amp;ldquo;AI가 만든 코드 리뷰&amp;rdquo;가 아니라, 목표는 &lt;b&gt;구현 결과가 승인된 스펙에서 벗어났는지 확인하는 것&lt;/b&gt;입니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예를 들어 팀에서 다음 같은 규칙을 두고 싶을 때 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NOT_READY 스펙은 구현으로 넘기지 않기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Critical/Major finding을 PR에서 먼저 노출하기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;구현 결과물이 아니라 요구사항 입력 품질을 먼저 관리하기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;구현 PR이 승인된 스펙과 어긋나는지 확인하기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설치는 CLI에서 다음 명령으로 사용할 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779032721990&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;specguard actions install-pr-review&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;또는 Codex에게 아래와 같이 요청할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779034330071&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@specguard SpecGuard PR Review 워크플로우를 설정해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다만, PR Review가 실제로 OpenAI 기반 리뷰를 수행하려면 GitHub Actions Secret에 SPECGUARD_OPENAI_API_KEY를 등록해야 합니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;선택적으로&amp;nbsp;아래&amp;nbsp;repository&amp;nbsp;variable도&amp;nbsp;설정할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779032748842&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SPECGUARD_PR_REVIEW_MODEL=gpt-5.4-nano
SPECGUARD_REVIEW_SPEC_PATHS=specs/your-feature-name&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 부분도 앞으로 Codex 플러그인에서 더 잘 도와주고 싶은 부분입니다. 예를 들어 사용자가 &quot;@SpecGuard PR Review를 설정해줘&quot;라고 요청하면, Codex가 다음을 순서대로 확인해주는 흐름입니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1.&amp;nbsp;현재&amp;nbsp;저장소와&amp;nbsp;브랜치&amp;nbsp;상태&amp;nbsp;확인 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. specguard CLI 설치 여부 확인 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3.&amp;nbsp;없으면&amp;nbsp;설치&amp;nbsp;안내&amp;nbsp;또는&amp;nbsp;설치&amp;nbsp;실행 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4.&amp;nbsp;PR&amp;nbsp;Review&amp;nbsp;workflow&amp;nbsp;생성&amp;nbsp;전&amp;nbsp;사용자&amp;nbsp;확인 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. specguard actions install-pr-review 실행 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6.&amp;nbsp;필요한&amp;nbsp;GitHub&amp;nbsp;Secret과&amp;nbsp;Variables&amp;nbsp;안내 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;7.&amp;nbsp;GitHub&amp;nbsp;연결이&amp;nbsp;되어&amp;nbsp;있으면&amp;nbsp;안전한&amp;nbsp;secret&amp;nbsp;등록&amp;nbsp;경로&amp;nbsp;안내&amp;nbsp;또는&amp;nbsp;보조 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;8.&amp;nbsp;연결이&amp;nbsp;없으면&amp;nbsp;수동&amp;nbsp;설정&amp;nbsp;방법&amp;nbsp;안내 &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;API Key를 플러그인이 임의로 만들거나 저장해서는 안 됩니다. Secret은 GitHub Actions Secret으로만 다뤄야 하고, 저장소에 커밋되어서는 안 됩니다. 이&amp;nbsp;보안&amp;nbsp;경계는&amp;nbsp;앞으로도&amp;nbsp;유지할&amp;nbsp;생각입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;설계하면서 중요하게 본 것&lt;/b&gt; &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard를 만들면서 중요하게 본 기준은 아래와 같습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;첫째,&amp;nbsp;CLI가&amp;nbsp;기준이&amp;nbsp;되어야&amp;nbsp;합니다. &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Codex 플러그인, GitHub Actions, PR Review는 모두 같은 SpecGuard CLI 결과를 읽어야 합니다. 플러그인 안에 별도의 리뷰 엔진을 만들면 동작이 갈라지고 유지보수가 어려워집니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;둘째,&amp;nbsp;기본&amp;nbsp;검사는&amp;nbsp;재현&amp;nbsp;가능해야&amp;nbsp;합니다. &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 기본 게이트는 API Key 없이 동작하는 휴리스틱 검사입니다. LLM 리뷰는 더 깊게 보고 싶을 때 선택적으로 붙이는 보조 경로에 가깝습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;셋째,&amp;nbsp;스펙&amp;nbsp;수정은&amp;nbsp;사용자가&amp;nbsp;결정해야&amp;nbsp;합니다. &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard는 finding을 보여주고 개선 방향을 제안할 수 있지만, 제품 요구사항 자체를 자동으로 바꾸는 것은 조심해야 합니다. 스펙을 바꾸는 것은 곧 제품 의도를 바꾸는 일이기 때문입니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;넷째,&amp;nbsp;구현&amp;nbsp;에이전트에게&amp;nbsp;넘기는&amp;nbsp;입력을&amp;nbsp;명확히&amp;nbsp;해야&amp;nbsp;합니다. &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AI 구현 에이전트에게 애매한 요구사항을 던지고 결과를 기다리는 대신, 검토된 스펙과 핸드오프 문서를 넘기는 방식을 지향합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재 한계와 앞으로 할 일&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpecGuard는 아직 초기 버전입니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;모든 스펙을 완벽하게 판별하지 못합니다. 특히 휴리스틱 기반 검사는 빠르고 재현 가능하지만, 모든 도메인 문제를 다 잡아내지는 못합니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재 가장 먼저 개선해야 할 방향은 아래와 같이 잡았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;불필요하게 깨지는 테스트 줄이기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핵심 로직 중심의 안정적인 테스트 유지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;휴리스틱 리뷰의 미탐/오탐률 개선&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Codex 플러그인 사용 흐름 개선&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;@SpecGuard 기반 PR Review 설정 UX 개선&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GitHub Secret/Variables 설정 안내 개선&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;한국어 스펙에서의 finding 품질 개선&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;특히 OSS 프로젝트로 운영하려면 테스트가 너무 많은 구현 세부사항에 묶이면 안 된다고 보고 있습니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핵심 동작은 지키되, 문서 문구나 내부 구조가 조금 바뀔 때마다 CI가 깨지는 형태는 줄이려고 합니다. &lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AI 코딩 도구는 앞으로 더 많이 쓰이게 될 것 같습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 AI에게 무엇을 만들지 알려주는 스펙이 약하면, 구현 속도가 빨라질수록 잘못된 방향으로 더 빠르게 갈 수 있습니다. SpecGuard는&amp;nbsp;그&amp;nbsp;전에&amp;nbsp;한&amp;nbsp;번&amp;nbsp;멈춰서&amp;nbsp;묻는&amp;nbsp;도구입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779032840602&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;이 스펙을 정말 구현 에이전트에게 넘겨도 되는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이&amp;nbsp;질문을&amp;nbsp;자동화된&amp;nbsp;게이트로&amp;nbsp;만들고&amp;nbsp;싶었습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;아직&amp;nbsp;초기&amp;nbsp;단계의&amp;nbsp;OSS&amp;nbsp;프로젝트지만,&amp;nbsp;AI&amp;nbsp;코딩을&amp;nbsp;사용하면서&amp;nbsp;&amp;ldquo;스펙이&amp;nbsp;약해서&amp;nbsp;구현이&amp;nbsp;틀어지는&amp;nbsp;문제&amp;rdquo;를&amp;nbsp;겪고&amp;nbsp;있다면&amp;nbsp;한&amp;nbsp;번&amp;nbsp;사용해보셔도&amp;nbsp;좋을&amp;nbsp;것&amp;nbsp;같습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;GitHub 저장소: &lt;a href=&quot;https://github.com/KoreaNirsa/spec-guard&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/KoreaNirsa/spec-guard&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;피드백과 기여도 환영합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재&amp;nbsp;이슈와&amp;nbsp;PR은&amp;nbsp;주로&amp;nbsp;영어로&amp;nbsp;관리하고&amp;nbsp;있지만,&amp;nbsp;한국어로&amp;nbsp;작성해주셔도&amp;nbsp;괜찮습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;특히 아래 피드백이 있으면 많은 도움이 될 것 같습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-&amp;nbsp;어떤&amp;nbsp;종류의&amp;nbsp;스펙에서&amp;nbsp;잘&amp;nbsp;맞는지 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-&amp;nbsp;어떤&amp;nbsp;finding이&amp;nbsp;과하거나&amp;nbsp;부족한지 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-&amp;nbsp;Codex&amp;nbsp;플러그인&amp;nbsp;흐름이&amp;nbsp;실제로&amp;nbsp;쓸&amp;nbsp;만한지 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-&amp;nbsp;PR&amp;nbsp;Review&amp;nbsp;방식이&amp;nbsp;팀&amp;nbsp;워크플로우에&amp;nbsp;맞는지 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-&amp;nbsp;한국어&amp;nbsp;스펙에서&amp;nbsp;어색하거나&amp;nbsp;부족한&amp;nbsp;부분이&amp;nbsp;있는지 &lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <author>KoreaNirsa</author>
      <guid isPermaLink="true">https://nirsa.tistory.com/487</guid>
      <comments>https://nirsa.tistory.com/487#entry487comment</comments>
      <pubDate>Mon, 18 May 2026 00:52:07 +0900</pubDate>
    </item>
  </channel>
</rss>