<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>범데이의 개발노트  </title>
    <link>https://bumday.tistory.com/</link>
    <description>반갑습니다!
개발자 범데이입니다 :)</description>
    <language>ko</language>
    <pubDate>Wed, 1 Jul 2026 18:13:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>범데이</managingEditor>
    <image>
      <title>범데이의 개발노트  </title>
      <url>https://tistory1.daumcdn.net/tistory/3827257/attach/45101c7496444dbb80be4293e1d5bb01</url>
      <link>https://bumday.tistory.com</link>
    </image>
    <item>
      <title>Windows Cursor 터미널 한글 깨짐, PowerShell 프로필로 해결하는 방법</title>
      <link>https://bumday.tistory.com/302</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor 터미널과 Git에서 한글이 깨지는 건 대부분 Windows 코드페이지(CP949) 때문이다. &lt;b&gt;PowerShell 프로필&lt;/b&gt;에 UTF-8 자동 전환 스크립트를 넣으면 터미널&amp;middot;Git 커밋까지 대부분 같이 해결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;절차&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: PowerShell 프로필 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로 (Windows PowerShell 5.1):&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;C:\Users\&amp;lt;사용자명&amp;gt;\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
&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;PowerShell 7+(pwsh)라면:&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;C:\Users\&amp;lt;사용자명&amp;gt;\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일이 없으면 새로 만들고, 아래 내용을 넣는다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;if ($Host.Name -eq 'ConsoleHost') {
    try {
        chcp 65001 | Out-Null
        [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
        [Console]::InputEncoding = [System.Text.Encoding]::UTF8
        $OutputEncoding = [System.Text.Encoding]::UTF8
    } catch {}
}
&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;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: &lt;span style=&quot;background-color: #dddddd;&quot;&gt;$PROFILE&lt;/span&gt;로 경로 확인, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Test-Path $PROFILE&lt;/span&gt;이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;True&lt;/span&gt;면 정상 생성.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: Cursor 재시작 후 확인&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모든 터미널 종료 (&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Ctrl+Shift+P&lt;/span&gt; &amp;rarr; &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Terminal: Kill All Terminals&lt;/span&gt;)&lt;/li&gt;
&lt;li&gt;Cursor 재시작 &amp;rarr; 새 터미널 열기&lt;/li&gt;
&lt;li&gt;아래로 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;chcp
Write-Output &quot;한글 테스트&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: Active code page: 65001이 나오고 &quot;한글 테스트&quot;가 깨지지 않으면 끝.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;한글 깨짐의 대부분 원인은 Git이 아니라 Windows 코드페이지(CP949)다.&lt;/li&gt;
&lt;li&gt;PowerShell 프로필에 UTF-8 전환 스크립트를 넣으면 터미널&amp;middot;Git 커밋까지 대부분 같이 해결된다.&lt;/li&gt;
&lt;li&gt;그래도 커밋 메시지가 깨지면, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;git commit -F&lt;/span&gt; + UTF-8 파일로 우회할 수 있다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Record/Trubble Shooting</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/302</guid>
      <comments>https://bumday.tistory.com/302#entry302comment</comments>
      <pubDate>Sun, 21 Jun 2026 23:53:27 +0900</pubDate>
    </item>
    <item>
      <title>Windows 비밀번호를 잊었을 때 포맷 없이 복구하는 방법</title>
      <link>https://bumday.tistory.com/301</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt; 회사 보안 프로그램이 주기적으로 비밀번호 변경을 요구해, 해당 프로그램 내에서 멍때리고 새로운 패스워드를 입력했다가..&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이후 로그인할때 기억하는 모든 패스워드를 쳐봐도 로그인이 되지 않았다..(ㅠㅠ)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows 로그인 비밀번호를 분실했을 때, 포맷 없이 기존 데이터를 유지한 채 &lt;b&gt;CMD 우회법&lt;/b&gt;으로 비밀번호를 강제 변경하는 방법이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다. 핵심은 설치 USB 복구 환경에서 접근성 실행 파일(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;utilman.exe&lt;/span&gt;)을 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;cmd.exe&lt;/span&gt;로 교체해, 로그인 화면에서 시스템 권한 CMD를 실행하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 상황에서 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 화면에서 비밀번호 입력이 막히고, Microsoft 계정 온라인 재설정도 보안 질문 분실 등으로 불가능한 경우&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XkUQ5/dJMcahY51sP/RbDkstZbi5ttu9Wzrf4dRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XkUQ5/dJMcahY51sP/RbDkstZbi5ttu9Wzrf4dRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XkUQ5/dJMcahY51sP/RbDkstZbi5ttu9Wzrf4dRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXkUQ5%2FdJMcahY51sP%2FRbDkstZbi5ttu9Wzrf4dRK%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;992&quot; height=&quot;758&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원리는 Windows 로그인 화면의 &lt;b&gt;접근성 버튼&lt;/b&gt;(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;utilman.exe&lt;/span&gt;)이 로그인 전에도 실행된다는 점을 이용하는 것이다. 이 파일을 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;cmd.exe&lt;/span&gt;로 교체하면 인증 없이 &lt;b&gt;시스템 권한 CMD&lt;/b&gt;가 실행된다. 복구 환경(WinPE)은 파일 시스템에 직접 접근할 수 있어 이 교체가 가능하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의: 적용 범위는 &lt;b&gt;로컬 계정&lt;/b&gt;에 한정된다. Microsoft 계정의 온라인 인증 방식에는 적용되지 않을 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&lt;b&gt;준비물&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정상 작동하는 다른 PC 1대 (Windows 설치 USB 제작용)&lt;/li&gt;
&lt;li&gt;8GB 이상 USB 메모리&lt;/li&gt;
&lt;li&gt;Microsoft 공식 홈페이지에서 받은 Windows 설치 미디어&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;절차&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 설치 USB로 부팅 후 CMD 진입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠긴 PC에 설치 USB를 꽂고 부팅한다. 제조사별 부팅 메뉴 키(F12, F11, Del 등)로 USB를 선택한 뒤, 언어 설정 화면에서 Shift + F10을 눌러 CMD를 실행한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: CMD 창이 뜨면 진입 성공.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: Windows 설치 드라이브 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복구 환경 기본 드라이브는 X:\이므로, 실제 Windows가 설치된 드라이브를 찾아야 한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;c:
dir
# Windows, Users 폴더가 있으면 올바른 드라이브
# 없으면 d:, e: 순으로 탐색
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: dir 결과에 Windows, Users 폴더가 보이면 해당 드라이브가 맞다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: utilman.exe 교체&lt;/h3&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;move C:\Windows\System32\utilman.exe C:\Windows\System32\utilman.exe.bak
copy C:\Windows\System32\cmd.exe C:\Windows\System32\utilman.exe
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: 두 명령 모두 1개 파일이 이동/복사되었습니다 메시지가 나오면 성공. USB를 뽑고 재시작한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계: 로그인 화면에서 비밀번호 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 화면 우측 하단의 접근성 버튼을 클릭하면 시스템 권한 CMD가 열린다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 계정 목록 확인
net user

# 비밀번호 변경 (계정명과 새 비밀번호를 입력)
net user [계정명] [새비밀번호]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: 명령을 잘 완료했습니다. 메시지가 뜨면 완료. 새 비밀번호로 로그인한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5단계: 원상복구 (보안상 필수)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 로그인이 잘 될것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 후 반드시 원본 파일을 복원한다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;del C:\Windows\System32\utilman.exe
move C:\Windows\System32\utilman.exe.bak C:\Windows\System32\utilman.exe
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: dir C:\Windows\System32\utilman.exe로 원본 크기의 파일이 복원됐는지 확인한다. 복원하지 않으면 누구나 로그인 화면에서 시스템 권한 CMD에 접근할 수 있는 보안 구멍이 남는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;복구 환경 CMD는 로그인 없이 시스템 파일에 접근할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;utilman.exe&lt;/span&gt;는 로그인 전 실행되므로, 이를 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;cmd.exe&lt;/span&gt;로 교체하면 시스템 권한 셸을 얻을 수 있다.&lt;/li&gt;
&lt;li&gt;net user 명령으로 인증 없이 로컬 계정 비밀번호를 변경할 수 있다.&lt;/li&gt;
&lt;li&gt;작업 후 반드시 원본 파일을 복원해 보안 구멍을 닫아야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; 항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 설명 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;utilman.exe&lt;/td&gt;
&lt;td&gt;Windows 접근성 관리자. 로그인 화면에서도 실행됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WinPE&lt;/td&gt;
&lt;td&gt;Windows Preinstallation Environment. 설치 USB 부팅 시 진입하는 경량 OS 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;net user&lt;/td&gt;
&lt;td&gt;Windows 로컬 사용자 계정을 관리하는 CMD 명령어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적용 범위&lt;/td&gt;
&lt;td&gt;로컬 계정에만 적용 가능. Microsoft 계정 온라인 인증 방식에는 적용되지 않을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>Window</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/301</guid>
      <comments>https://bumday.tistory.com/301#entry301comment</comments>
      <pubDate>Thu, 18 Jun 2026 18:15:45 +0900</pubDate>
    </item>
    <item>
      <title>운영 미배포 작업을 보류 브랜치로 분리해 관리하는 방법</title>
      <link>https://bumday.tistory.com/300</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;운영 배포 전 main에만 올린 작업을 취소해야 하는데, 이미 개발 환경 배포 브랜치에는 반영돼서 그쪽은 건드릴 수 없는 상황. &lt;b&gt;별도 보류 브랜치에 작업을 먼저 백업한 뒤, main에서는 git revert로 되돌리고, 운영 배포 시점에 보류 브랜치를 다시 합치는 방식&lt;/b&gt;으로 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 흐름이 아래처럼 분리된 환경을 가정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;feat/* &amp;rarr; main &amp;rarr; 개발 배포 브랜치 &amp;rarr; 개발 환경 배포&lt;/li&gt;
&lt;li&gt;feat/* &amp;rarr; main &amp;rarr; 운영 배포 브랜치 &amp;rarr; 운영 환경 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 main에 올리고 개발 배포 브랜치까지 merge해 개발 환경에는 이미 반영됐지만, 운영 배포 브랜치에는 아직 merge하지 않은 시점에 다음 배포 일정이 미뤄지는 경우가 있다. 이때 main은 다음 운영 배포 전까지 깨끗한 상태를 유지해야 하므로, 해당 작업만 main에서 제외하면서도 이미 배포된 개발 환경은 그대로 둬야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 git reset --hard로 되돌리면 이미 push한 공유 브랜치의 히스토리가 삭제돼 위험하다. 대신 히스토리를 보존하면서 변경만 반대로 적용하는 git revert를 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;준비물&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업이 올라간 main 커밋 해시 (git log main --oneline으로 확인)&lt;/li&gt;
&lt;li&gt;작업을 보관할 보류 브랜치 (예: feat/hold) &amp;mdash; 없으면 새로 생성&lt;/li&gt;
&lt;li&gt;로컬 저장소에 main, 개발 배포 브랜치, 보류 브랜치에 대한 push 권한&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;절차&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 보류 브랜치에 작업 백업&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 배포 전까지 작업을 보관할 브랜치로 이동해, main에 올렸던 작업 커밋을 cherry-pick으로 가져온다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;git checkout feat/hold
git cherry-pick &amp;lt;main에 올린 작업 커밋해시&amp;gt;
git push origin feat/hold
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: git log feat/hold --oneline에 해당 커밋이 보이면 백업 완료.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: main에서만 작업 되돌리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main을 최신 상태로 받은 뒤, 해당 커밋을 revert한다. 작업 커밋이 여러 개면 &lt;b&gt;최신 커밋부터 하나씩&lt;/b&gt; revert한다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;git checkout main
git pull origin main
git revert &amp;lt;작업 커밋해시&amp;gt; --no-edit
git push origin main
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--no-edit은 revert 커밋 메시지를 편집기 없이 기본값으로 바로 쓰겠다는 옵션이다. 머지 커밋을 revert해야 하는 경우엔 부모 브랜치 기준을 지정해야 한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;git revert -m 1 &amp;lt;머지커밋해시&amp;gt; --no-edit
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-m 1은 merge 시 main 쪽(첫 번째 부모) 기준으로 되돌린다는 의미다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: git log main --oneline에 Revert &quot;...&quot; 커밋이 새로 생기고, 해당 변경분이 코드에서 사라졌는지 확인.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 개발 배포 브랜치는 그대로 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경에는 이미 반영된 상태이므로 &lt;b&gt;아무 작업도 하지 않는다&lt;/b&gt;. 운영 배포 전까지 main &amp;rarr; 개발 배포 브랜치 merge 자체를 하지 않는 것이 중요하다. revert가 그쪽에 들어가면 개발 환경에서 해당 기능이 사라질 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: 개발 배포 브랜치의 최근 merge 로그에 main의 revert 커밋이 포함되지 않았는지 확인.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계: 운영 배포 시점에 다시 합치기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 배포 일정이 오면, 보류 브랜치를 main에 merge하고, 이어서 운영 배포 브랜치로 merge한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;git checkout main
git merge feat/hold
git push origin main
# 이후 운영 배포 브랜치로 merge
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 방법: main에 보류 브랜치의 커밋이 다시 들어왔는지, revert로 빠졌던 변경이 복원됐는지 코드 diff로 확인.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;운영 미배포 작업은 처음부터 보류 브랜치에서 진행하고, 배포 시점에만 main으로 merge하는 흐름을 기본으로 삼는다&lt;/li&gt;
&lt;li&gt;git revert는 히스토리를 지우지 않고 변경만 반대로 적용하므로, 이미 push한 공유 브랜치에서는 reset --hard보다 항상 안전하다&lt;/li&gt;
&lt;li&gt;revert 대상은 &lt;b&gt;내 작업 커밋만&lt;/b&gt;이며, 단순히 원격 브랜치를 동기화한 merge 커밋은 되돌릴 필요 없다&lt;/li&gt;
&lt;li&gt;브랜치 작업 외에, 프론트엔드 변경이 있었다면 push 전 최소한 빌드 검사(예: pnpm build &amp;mdash; 타입 검사 + 프로덕션 빌드)로 컴파일 오류 여부를 확인하는 과정도 함께 거쳐야 한다&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Git</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/300</guid>
      <comments>https://bumday.tistory.com/300#entry300comment</comments>
      <pubDate>Thu, 18 Jun 2026 18:05:52 +0900</pubDate>
    </item>
    <item>
      <title>eGovFrame 채번 테이블 불일치로 메뉴 ID가 중복된 사례</title>
      <link>https://bumday.tistory.com/299</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;eGovFrame 기반 시스템에서 메뉴 등록 후 &lt;b&gt;웹페이지가 멈추거나&lt;/b&gt; Duplicate Key 오류가 발생했다. 최종 원인은 &lt;b&gt;채번 테이블의 NEXT_ID가 실제 데이터 최댓값보다 작았고&lt;/b&gt;, 대상 테이블에 &lt;b&gt;PK 제약이 없어&lt;/b&gt; 중복 삽입이 즉시 막히지 않았던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관리 화면에서 &lt;b&gt;메뉴 등록&amp;middot;저장&lt;/b&gt; 시 오류가 발생하거나, 저장 후 &lt;b&gt;메뉴 트리&amp;middot;특정 페이지가 로딩되지 않는다&lt;/b&gt;(먹통)&lt;/li&gt;
&lt;li&gt;애플리케이션 로그에 Duplicate entry / Unique constraint violation 류 메시지가 남는다&lt;/li&gt;
&lt;li&gt;DB에는 이미 &lt;b&gt;동일 entityId(메뉴 ID)&lt;/b&gt; 를 가진 행이 2건 이상 존재할 수 있다&lt;/li&gt;
&lt;li&gt;처음에는 애플리케이션 버그&amp;middot;캐시 문제를 의심했으나, &lt;b&gt;채번 값과 실데이터 불일치&lt;/b&gt; 쪽으로 좁혀졌다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;어떻게 좁혔나&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 오류 시점&amp;middot;경로 확인&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;프로그램 경로(관리 화면 INSERT)에서만 재현되는지, 조회만으로도 깨지는지 구분했다&lt;/li&gt;
&lt;li&gt;운영 DB &lt;b&gt;수동 INSERT&amp;middot;UPDATE 이력&lt;/b&gt;이 있는지 확인했다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 채번 테이블 vs 실데이터 비교&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; &lt;span style=&quot;text-align: start;&quot;&gt;검증&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;채번 테이블 NEXT_ID (예: TABLE_NAME = 'MENU_ID')&lt;/td&gt;
&lt;td&gt;예: &lt;b&gt;105&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실제 메뉴 테이블 MAX(entityId)&lt;/td&gt;
&lt;td&gt;예: &lt;b&gt;108&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프로그램이 다음에 발급할 ID&lt;/td&gt;
&lt;td&gt;&lt;b&gt;105&lt;/b&gt; &amp;rarr; 이미 존재하는 ID와 &lt;b&gt;충돌&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 제약조건&amp;middot;중복 데이터 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- PK/UNIQUE 없음 여부 (메타 조회 또는 스키마 확인)
-- 중복 entityId 존재 여부
SELECT entity_id, COUNT(*)
FROM menu_info
GROUP BY entity_id
HAVING COUNT(*) &amp;gt; 1;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PK가 없으면 INSERT 시점에 DB가 막지 못하고, &lt;b&gt;이후 조회&amp;middot;조인&amp;middot;트리 구성&lt;/b&gt; 단계에서 오류가 터질 수 있다&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;eGovFrame 테이블 기반 채번 동작&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EgovTableIdGnrServiceImpl 은 &lt;b&gt;채번 전용 테이블&lt;/b&gt;에서 TABLE_NAME 별 NEXT_ID 를 읽고 +1 UPDATE 후 반환한다&lt;/li&gt;
&lt;li&gt;blockSize=1 이면 호출마다 &lt;b&gt;SELECT &amp;rarr; UPDATE&lt;/b&gt; 를 수행한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;bean name=&quot;entityIdGnrService&quot;
    class=&quot;egovframework.rte.fdl.idgnr.impl.EgovTableIdGnrServiceImpl&quot;
    destroy-method=&quot;destroy&quot;&amp;gt;
    &amp;lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot; /&amp;gt;
    &amp;lt;property name=&quot;strategy&quot;   ref=&quot;menuStrategy&quot; /&amp;gt;
    &amp;lt;property name=&quot;blockSize&quot;  value=&quot;1&quot;/&amp;gt;
    &amp;lt;property name=&quot;table&quot;      value=&quot;SEQ_TABLE&quot;/&amp;gt;
    &amp;lt;property name=&quot;tableName&quot;  value=&quot;MENU_ID&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;&lt;/code&gt;&lt;/pre&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;컬럼&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TABLE_NAME&lt;/td&gt;
&lt;td&gt;채번 대상 식별자 (예: MENU_ID)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NEXT_ID&lt;/td&gt;
&lt;td&gt;다음 발급할 ID&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;h3 data-ke-size=&quot;size23&quot;&gt;불일치가 생긴 경로&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;운영 중 &lt;b&gt;DB에서 직접&lt;/b&gt; 메뉴 행을 INSERT&amp;middot;UPDATE 하면서 entityId 를 &lt;b&gt;수동 지정&lt;/b&gt;했다&lt;/li&gt;
&lt;li&gt;이때 &lt;b&gt;채번 테이블의 NEXT_ID 는 갱신되지 않았다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;실데이터 MAX(entityId) &amp;ge; 채번 NEXT_ID 상태가 되었다&lt;/li&gt;
&lt;li&gt;이후 프로그램이 메뉴 INSERT 시 &lt;b&gt;이미 존재하는 ID&lt;/b&gt; 를 다시 발급받아 충돌했다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PK 미설정이 증상을 악화시킨 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대상 테이블 entityId 에 &lt;b&gt;PK&amp;middot;UNIQUE 가 없으면&lt;/b&gt; 중복 INSERT 가 DB에서 즉시 거부되지 않는다&lt;/li&gt;
&lt;li&gt;Duplicate Key 는 &lt;b&gt;늦게&lt;/b&gt; 드러나거나, 조회 로직이 &lt;b&gt;단건 가정&lt;/b&gt;일 때 &lt;b&gt;페이지 전체가 멈춘 것처럼&lt;/b&gt; 보인다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 채번 값 보정&lt;/b&gt; &amp;mdash; 실데이터 최댓값 + 1 로 맞춘다&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UPDATE seq_table
SET next_id = (SELECT MAX(entity_id) + 1 FROM menu_info)
WHERE table_name = 'MENU_ID';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 중복 데이터 정리&lt;/b&gt; &amp;mdash; 위 중복 조회 쿼리로 걸린 행을 &lt;b&gt;업무 규칙에 맞게&lt;/b&gt; 병합&amp;middot;삭제한다 (선행 필수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) PK 또는 UNIQUE 제약 추가&lt;/b&gt; &amp;mdash; 향후 수동&amp;middot;프로그램 경로 모두에서 중복 삽입을 원천 차단한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) 운영 DB 직접 수정 가이드&lt;/b&gt; &amp;mdash; 수동으로 ID 를 넣을 때는 &lt;b&gt;반드시 채번 테이블 NEXT_ID 도 동기화&lt;/b&gt;하도록 Runbook 에 명시한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;eGovFrame &lt;b&gt;테이블 채번&lt;/b&gt;은 SEQ_TABLE.NEXT_ID 만 믿으면 안 된다 &amp;mdash; &lt;b&gt;실데이터 MAX(id)&lt;/b&gt; 와 주기적으로 대조한다.&lt;/li&gt;
&lt;li&gt;운영 DB &lt;b&gt;수동 INSERT&lt;/b&gt; 후 채번 불일치가 가장 흔한 원인이다 &amp;mdash; 수동 작업 시 &lt;b&gt;채번 동기화&lt;/b&gt;를 절차에 넣는다.&lt;/li&gt;
&lt;li&gt;ID 컬럼에 &lt;b&gt;PK&amp;middot;UNIQUE 가 없으면&lt;/b&gt; 중복이 조용히 쌓이고, &lt;b&gt;조회&amp;middot;트리 화면&lt;/b&gt;에서 늦게 터진다.&lt;/li&gt;
&lt;li&gt;메뉴&amp;middot;권한&amp;middot;코드처럼 &lt;b&gt;전역 참조되는 ID&lt;/b&gt; 는 충돌 시 영향 범위가 넓다 &amp;mdash; 등록 실패 로그와 &lt;b&gt;MAX(id) vs NEXT_ID&lt;/b&gt; 를 함께 본다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참고: 발생 오류 유형&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;레이어&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;Duplicate entry, Unique constraint violation (PK 설정 후에는 INSERT 시점에 즉시)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;애플리케이션&lt;/td&gt;
&lt;td&gt;중복 ID 로 인한 메뉴 조회 실패, Null/다건 처리 오류, 페이지 로딩 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>Framework</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/299</guid>
      <comments>https://bumday.tistory.com/299#entry299comment</comments>
      <pubDate>Tue, 16 Jun 2026 19:03:02 +0900</pubDate>
    </item>
    <item>
      <title>싱글톤 Bean 공유 상태로 동시 Ajax 응답이 섞인 사례</title>
      <link>https://bumday.tistory.com/298</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;화면에서 &lt;b&gt;API1은 1건, API2는 3건&lt;/b&gt;이 내려와야 하는데, Network 탭에서 &lt;b&gt;둘 다 3건&lt;/b&gt;으로 보이는 현상이 간헐적으로 발생했다. SQL&amp;middot;파라미터&amp;middot;배포를 의심했지만, 최종 원인은 &lt;b&gt;Spring 싱글톤 Bean이 공유 응답 객체를 동시 요청이 덮어쓴 것&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팝업(또는 특정 화면) 로드 시 Ajax &lt;b&gt;2개가 동시에&lt;/b&gt; 호출된다&lt;/li&gt;
&lt;li&gt;API1 응답은 &lt;b&gt;1건&lt;/b&gt;이어야 정상인데, Network에서 &lt;b&gt;3건&lt;/b&gt;으로 보이는 경우가 있다&lt;/li&gt;
&lt;li&gt;API2 응답도 &lt;b&gt;3건&lt;/b&gt;이며, &lt;b&gt;API1과 JSON 내용이 완전히 동일&lt;/b&gt;하다&lt;/li&gt;
&lt;li&gt;드롭다운 등 UI에 &lt;b&gt;같은 라벨&amp;middot;다른 코드값&lt;/b&gt;이 여러 건 노출되고, 잘못된 값이 저장될 수 있다&lt;/li&gt;
&lt;li&gt;API1&amp;middot;SQL을 &lt;b&gt;단독 호출&lt;/b&gt;하면 항상 1건 &amp;rarr; DB&amp;middot;SQL&amp;middot;파라미터 문제는 아닌 것으로 보였다&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떻게 좁혔나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) SQL&amp;middot;파라미터 소거&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;검증&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API1 SQL 직접 실행&lt;/td&gt;
&lt;td&gt;&lt;b&gt;항상 1건&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;curl 등으로 API1 단독 호출&lt;/td&gt;
&lt;td&gt;&lt;b&gt;항상 1건&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버 sql.log의 API1 실행 SQL&lt;/td&gt;
&lt;td&gt;필터 조건 &lt;b&gt;정상 포함&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요청 파라미터 (예: parentId)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;정상 전달&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 결정적 로그&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;[시각]  POST .../api1.json   &amp;rarr; SQL 필터 O, DB 1건
[시각]  POST .../api2.json   &amp;rarr; SQL 필터 X, DB 3건  (수 ms 차이)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL은 각각 정상인데 API1 Response만 3건 &amp;rarr; &lt;b&gt;DB 이후 레이어&lt;/b&gt;를 의심했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 코드 추적&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service&amp;middot;Controller에 &lt;b&gt;인스턴스 필드로 응답 객체를 하나만 두고&lt;/b&gt;, API1&amp;middot;API2가 &lt;b&gt;거의 동시에&lt;/b&gt; setResult 하는 구조였다.&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;&lt;b&gt;원인&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글톤 + 공유 가변 상태&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Service&amp;middot;@Controller는 기본 &lt;b&gt;싱글톤&lt;/b&gt; &amp;rarr; 모든 HTTP 요청이 &lt;b&gt;같은 Bean 인스턴스&lt;/b&gt;를 쓴다&lt;/li&gt;
&lt;li&gt;요청마다 새 응답 객체를 만들지 않고, &lt;b&gt;필드 하나&lt;/b&gt;에 결과를 넣고 그 참조를 반환했다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
public class SomeService {
    private ApiResponse response = new ApiResponse();  // 모든 요청이 공유

    public ApiResponse getListA(String parentId) {
        List&amp;lt;Item&amp;gt; list = dao.queryA(parentId);  // 예: 3건
        response.setResult(list);
        return response;
    }

    public ApiResponse getListB(String parentId) {
        List&amp;lt;Item&amp;gt; list = dao.queryB(parentId);  // 예: 1건 (필터 적용)
        response.setResult(list);  // 같은 객체를 덮어씀
        return response;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;경쟁 시나리오 (Race Condition)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Thread-A (API1)                  Thread-B (API2)
───────────────                  ───────────────
DB 조회 &amp;rarr; 1건
                                 DB 조회 &amp;rarr; 3건
response.setResult(1건)
                                 response.setResult(3건)  &amp;larr; 덮어씀
return &amp;rarr; 클라이언트에 3건      return &amp;rarr; 3건
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;마지막에 setResult한 쪽&lt;/b&gt;이 양쪽 HTTP 응답 모두에 반영될 수 있다&lt;/li&gt;
&lt;li&gt;API2가 나중이면 &lt;b&gt;API1 Response에도 3건&lt;/b&gt;이 나간다 &amp;rarr; 두 JSON이 &lt;b&gt;완전히 동일&lt;/b&gt;해진다&lt;/li&gt;
&lt;li&gt;스레드 스케줄링에 따라 1건/3건이 &lt;b&gt;간헐적으로&lt;/b&gt; 바뀌어 보인다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 단독 호출은 항상 정상인가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;curl&amp;middot;SQL 직접 실행 = &lt;b&gt;스레드 1개&lt;/b&gt; &amp;rarr; 경쟁 없음&lt;/li&gt;
&lt;li&gt;화면 로드 = API1&amp;middot;API2 &lt;b&gt;동시 Ajax&lt;/b&gt; &amp;rarr; 재현 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청마다 새 응답 객체를 생성&lt;/b&gt;하도록 변경했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private ApiResponse successResult(Object data) {
    ApiResponse response = new ApiResponse();
    response.setResult(data);
    response.setResultCode(&quot;OK&quot;);
    return response;
}

public ApiResponse getListB(String parentId) {
    List&amp;lt;Item&amp;gt; list = dao.queryB(parentId);
    return successResult(list);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller의 공유 응답 필드도 제거하고, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;return service.method(...)&lt;/span&gt; 로 &lt;b&gt;직접 반환&lt;/b&gt;하도록 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;SQL 로그 정상 + API Response 비정상&lt;/b&gt;이면 DB가 아니라 &lt;b&gt;응답 객체를 만드는 Java 레이어&lt;/b&gt;를 본다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단독 호출 OK / 동시 호출 NG&lt;/b&gt;는 &lt;b&gt;스레드 경쟁&lt;/b&gt;의 강한 힌트다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;싱글톤 Bean에 요청별 데이터를 인스턴스 필드로 두지 않는다&lt;/b&gt; &amp;mdash; 메서드 지역 변수로 생성 후 반환한다.&lt;/li&gt;
&lt;li&gt;Ajax를 &lt;b&gt;2개 이상 동시에&lt;/b&gt; 쏘는 화면은, 공유 상태 버그가 &lt;b&gt;간헐적으로만&lt;/b&gt; 터져 찾기 어렵다 &amp;mdash; Network에서 &lt;b&gt;URL별 Response&lt;/b&gt;와 &lt;b&gt;sql.log&lt;/b&gt;를 반드시 대조한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참고: API1 vs API2 (예시)&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;SQL&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;예상 건수&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;API1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;드롭다운 목록&lt;/td&gt;
&lt;td&gt;필터 적용 (예: 특정 타입만)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;1건&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;API2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;부가 정보&amp;middot;안내&lt;/td&gt;
&lt;td&gt;필터 없음 (전체 트리)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;3건&lt;/b&gt; (예: 상위&amp;middot;중간&amp;middot;하위 코드)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 API를 &lt;b&gt;항상 동시 호출&lt;/b&gt;하는 구조에서, 수정 전에는 API1 응답에 API2 결과가 섞여 잘못된 option이 노출될 수 있었다.&lt;/p&gt;</description>
      <category>Record/Trubble Shooting</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/298</guid>
      <comments>https://bumday.tistory.com/298#entry298comment</comments>
      <pubDate>Tue, 16 Jun 2026 18:50:30 +0900</pubDate>
    </item>
    <item>
      <title>외부망에서만 API가 Empty Response로 끊긴 사례 해결</title>
      <link>https://bumday.tistory.com/297</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;외부망에서만 API 임시저장이 &lt;b&gt;응답 없이 끊겼다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST 직후 &lt;b&gt;HTTP 상태&amp;middot;본문 없음&lt;/b&gt; (약 40ms, 타임아웃 아님)&lt;/li&gt;
&lt;li&gt;브라우저 Network Timing: &lt;b&gt;Waiting(TTFB) 구간 없음&lt;/b&gt; &amp;rarr; 응답 첫 바이트조차 오기 전에 끊김 (아래 참고)&lt;/li&gt;
&lt;li&gt;브라우저: ERR_EMPTY_RESPONSE / curl: (52) Empty reply from server&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부망에서는 동일 기능 정상&lt;/b&gt;, &lt;b&gt;외부망에서만&lt;/b&gt; 재현&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 폼 null, 임시저장 불러오기, DB 오류를 의심했지만, curl로도 재현되고 &lt;b&gt;body를 줄이면 400 JSON&lt;/b&gt;이 돌아와 &lt;b&gt;앱&amp;middot;경로는 살아 있음&lt;/b&gt;이 확인됐다. API가 통째로 죽은 상황은 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;어떻게 좁혔나&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) body 크기로 층 나누기&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;body&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최소 필드만&lt;/td&gt;
&lt;td&gt;&lt;b&gt;400 JSON&lt;/b&gt; (앱 도달)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실제 신청 form 전체&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Empty&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 필드 하나씩 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표자 변경만, 면적 변경만, 긴 주소만 &amp;rarr; 모두 &lt;b&gt;400&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트에 &lt;b&gt;직영/위탁 운영형태&lt;/b&gt; 필드만 넣으면 &amp;rarr; &lt;b&gt;Empty&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 결정적 테스트&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;운영형태 코드 값&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DIR&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ABC / DI&lt;/td&gt;
&lt;td&gt;400 JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;DIR 문자열&lt;/b&gt;이 트리거. 필드 이름이나 한글 주소 길이가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DIR은 업무 코드다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;직영/위탁 운영형태&lt;/b&gt;를 구분하는 공통코드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DIR = 직영(Direct)&lt;/b&gt; 등, 업무상 &lt;b&gt;정상 코드값&lt;/b&gt; (윈도우 DIR 명령과 무관)&lt;/li&gt;
&lt;li&gt;업체 마스터&amp;middot;신청 form에 이미 들어 있어 POST body에 &lt;b&gt;&quot;DIR&quot;&lt;/b&gt; 코드값이 포함되어 실림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WAF(웹 방화벽)가 끊는 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;body를 &lt;b&gt;실행하지 않고&lt;/b&gt; 문자열만 검사&lt;/li&gt;
&lt;li&gt;&quot;DIR&quot; &amp;rarr; directory / path traversal 류 &lt;b&gt;시그니처 오탐&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;차단 시 403이 아니라 &lt;b&gt;연결만 종료&lt;/b&gt; &amp;rarr; Empty Response&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내부 OK / 외부 NG&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부망은 WAF를 타지 않거나 정책이 느슨하고, 외부망 인터넷 구간 WAF에서만 막힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공통코드 값 변경.&lt;/b&gt; 직영/위탁 운영형태 공통코드에서 &lt;b&gt;DIR 코드값을 WAF에 걸리지 않는 값으로 변경&lt;/b&gt;했다. 화면에 보이는 직영/위탁 구분(cdNm 등)은 그대로 두고, POST body에 &quot;DIR&quot; 문자열이 실리지 않게 한 것이 최종 조치다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(WAF 예외&amp;middot;POST 필드 제거 등도 대안이 될 수 있으나, 이번에는 공통코드 변경으로 해결.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Empty Response&lt;/b&gt;는 앞단 차단 신호 &amp;mdash; &lt;b&gt;TTFB* 없이&lt;/b&gt; 수십 ms면 WAF&amp;middot;프록시를 먼저 본다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부 OK / 외부 NG&lt;/b&gt;는 WAF 경로 차이의 강한 힌트.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;curl 최소 body vs 전체 body&lt;/b&gt;로 문제 필드를 이분할 수 있다.&lt;/li&gt;
&lt;li&gt;업무상 정상인 짧은 코드(DIR, NAT &amp;hellip;)도 &lt;b&gt;보안 시그니처와 충돌&lt;/b&gt;할 수 있다 &amp;mdash; 공통코드 설계&amp;middot;WAF 예외&amp;middot;전송 필드 최소화를 함께 검토한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;* &lt;b&gt;TTFB&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(Time To First Byte) = 요청을 보낸 뒤&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;HTTP 응답의 첫 바이트&lt;/b&gt;가 도착하기까지 걸린 시간 (&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Chrome 개발자 도구 Network &amp;rarr; Timing의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Waiting&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;구간과 같다.)&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;[요청 전송] &amp;rarr; [서버&amp;middot;WAF 처리] &amp;rarr; [첫 바이트] &amp;rarr; [본문 전체]
                              &amp;uarr; TTFB
&lt;/code&gt;&lt;/pre&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Timing&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Waiting(TTFB) 있음&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;200&amp;middot;400 등 &lt;b&gt;HTTP 응답이 시작&lt;/b&gt;됨 (앱&amp;middot;프록시까지 도달)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Waiting(TTFB) 없음&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;응답 시작 전&lt;/b&gt; 연결 종료 &amp;rarr; Empty Response, ERR_EMPTY_RESPONSE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번처럼 TTFB 없이 ~40ms에 끊기면, DB 느림&amp;middot;30초 타임아웃보다 &lt;b&gt;앞단(WAF 등)에서 body 검사 후 차단&lt;/b&gt;을 먼저 의심한다.&lt;/p&gt;</description>
      <category>Record/Trubble Shooting</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/297</guid>
      <comments>https://bumday.tistory.com/297#entry297comment</comments>
      <pubDate>Tue, 16 Jun 2026 18:40:35 +0900</pubDate>
    </item>
    <item>
      <title>[Troubleshooting] Maven &amp;quot;was cached in the local repository&amp;quot; &amp;mdash; 폐쇄망 빌드 서버에서 캐시된 실패 처리</title>
      <link>https://bumday.tistory.com/296</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;parent POM을 Nexus에 올렸는데도 빌드가 또 실패했다. 원인은 Maven이 이전에 실패한 해석 결과를 로컬 캐시에 저장해두고 계속 실패로 처리하고 있었기 때문이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 증상&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parent POM을 Nexus에 올리고 다시 빌드를 돌렸는데 또 같은 에러가 떴다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;[ERROR] Failed to execute goal on project my-project:
  Could not resolve dependencies ...
    Failure to find com.github.librepdf:openpdf-parent:pom:1.3.30.jaspersoft.2
    in http://nexus.internal:8081/repository/maven-public/
    was cached in the local repository,
    resolution will not be reattempted until the update interval
    of nexus has elapsed or updates are forced&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf-parent&lt;/span&gt;는 분명히 Nexus에 올라가 있는데 왜 여전히 실패하는지 이해가 되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 원인 &amp;mdash; Maven의 네가티브 캐시&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven은 한 번 원격 레포에서 아티팩트 다운로드에 실패하면, 실패 정보를 로컬 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt;에 캐시한다. 다음 실행 시에는 원격 레포를 다시 조회하지 않고 캐시된 실패를 그대로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네가티브 마크 파일이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt;에 남아 있으면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;was cached&lt;/span&gt; 메시지가 뜨면서 설정된 인터벌이 지나기 전까지 재시도하지 않는다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;C:\Users\&amp;lt;agent&amp;gt;\.m2\repository\com\github\librepdf\openpdf-parent\1.3.30.jaspersoft.2\
    openpdf-parent-1.3.30.jaspersoft.2.pom.lastUpdated  &amp;larr; 이게 문제&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;.lastUpdated&lt;/span&gt; 파일에는 실패한 시간과 실패한 URL이 담겨 있다. Maven은 이것을 보고 &quot;이미 실패한 거 알아. 인터벌 안 지났으니 다시 안 찾아.&quot;라고 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;빌드 에이전트가 Nexus 업로드 이전 시점에 이미 캐시를 만들어둔 것&lt;/b&gt;이 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&lt;b&gt;3. 해결 &amp;mdash; -U 플래그로 강제 업데이트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt; (&lt;span style=&quot;background-color: #dddddd;&quot;&gt;--update-snapshots&lt;/span&gt;) 플래그를 추가하면, 캐시된 네가티브 마크를 무시하고 원격 레포를 직접 확인한다. TeamCity Maven Build Step의 &lt;b&gt;Goals&lt;/b&gt;에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt;를 추가했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# 변경 전
clean package -DskipTests

# 변경 후
clean package -DskipTests -U&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt;는 SNAPSHOT 업데이트와 &lt;b&gt;실패한 해석 캐시 무효화&lt;/b&gt; 두 가지를 동시에 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 주의사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt;는 빌드마다 원격 레포를 업데이트 확인하므로 &lt;b&gt;빌드 에이전트가 완전한 폐쇄망임을 전제로&lt;/b&gt; 한다. 원격 접근이 전혀 안 되는 환경이라면 아래 선택지를 사용할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;선택지&lt;/td&gt;
&lt;td&gt;방법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.lastUpdated 직접 삭제&lt;/td&gt;
&lt;td&gt;Get-ChildItem -Recurse -Filter *.lastUpdated | Remove-Item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.m2 전체 삭제&lt;/td&gt;
&lt;td&gt;빌드 에이전트의 캐시를 완전히 초기화 후 재시도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;업로드 선행 확인&lt;/td&gt;
&lt;td&gt;빌드 시작 전 Nexus에 정상 업로드되었는지 스크립트로 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 마무리&lt;/b&gt;&lt;/h2&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;빌드 에이전트가 빌드 시도 &amp;rarr; 실패 &amp;rarr; &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.lastUpdated&lt;/span&gt; 생성&lt;/li&gt;
&lt;li&gt;해당 아티팩트를 Nexus에 업로드&lt;/li&gt;
&lt;li&gt;다시 빌드 시도 &amp;rarr; Maven이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.lastUpdated&lt;/span&gt;를 보고 캐시된 실패로 판단 &amp;rarr; 또 실패&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt;를 주면 Maven이 캐시를 무시하고 Nexus를 직접 조회하므로 이 순환 고리를 끊을 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;빌드 #4: clean package -DskipTests -U  &amp;rarr; 성공  &lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폐쇄망에서 Nexus에 파일을 올린 후에도 빌드가 실패한다면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.lastUpdated&lt;/span&gt; 캐시를 먼저 의심한다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;-U&lt;/span&gt; 하나로 해결된다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Java/Maven</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/296</guid>
      <comments>https://bumday.tistory.com/296#entry296comment</comments>
      <pubDate>Wed, 3 Jun 2026 10:27:02 +0900</pubDate>
    </item>
    <item>
      <title>[Troubleshooting] Maven parent POM이 Nexus에 없어서 빌드 실패 &amp;mdash; mvn dependency:list의 함정</title>
      <link>https://bumday.tistory.com/295</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;폐쇄망 Nexus에 의존성을 올렸는데도 빌드가 실패했다. 원인은 mvn dependency:list가 parent POM을 출력하지 않는다는 점이었다. 이 함정과 해결 방법을 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 증상&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt;에서 의존성을 수집해 Nexus에 올리고 폐쇄망 TeamCity 빌드를 돌렸더니 아래 에러가 떴다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;Could not resolve dependencies for project ...
  Could not find artifact com.github.librepdf:openpdf-parent:pom:1.3.30.jaspersoft.2
    in nexus (http://nexus.internal:8081/repository/maven-public/)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf&lt;/span&gt;는 분명히 올렸는데 왜 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf-parent&lt;/span&gt;가 없다고 할까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 원인 &amp;mdash; mvn dependency:list의 함정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven 빌드 시 각 아티팩트의 POM을 읽어서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;parent&amp;gt;&lt;/span&gt; 태그를 따라 부모 POM도 함께 다운로드한다. 그런데 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;는 &lt;b&gt;실제 빌드에 사용되는 JAR만&lt;/b&gt; 출력하고, 모델 해석에 필요한 &lt;b&gt;parent POM은 목록에 포함시키지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf&lt;/span&gt; JAR를 올렸더라도, Maven이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf.pom&lt;/span&gt;을 읽어보면 아래와 같다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;xml&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;&amp;lt;parent&amp;gt;
  &amp;lt;groupId&amp;gt;com.github.librepdf&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;openpdf-parent&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;1.3.30.jaspersoft.2&amp;lt;/version&amp;gt;
&amp;lt;/parent&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 parent POM까지 Nexus에 있어야 빌드가 성공한다. 하지만 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;에는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;openpdf-parent&lt;/span&gt;가 전혀 나타나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 해결 &amp;mdash; POM의 parent 체인을 재귀적으로 탐색&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수집 스크립트에 Step 4를 추가했다. 이미 수집된 모든 POM 파일을 파싱해서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;parent&amp;gt;&lt;/span&gt; 요소를 찾고, 해당 parent POM도 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt;에서 복사한다. 복사한 parent POM에서 또 parent를 찾는 식으로 BFS(너비 우선 탐색)로 체인 전체를 추적했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;powershell&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# POM에서 &amp;lt;parent&amp;gt; 정보 추출
function Get-PomParent($pomPath) {
    try {
        $doc = New-Object System.Xml.XmlDocument
        $doc.Load($pomPath)  # [xml] 캐스트 대신 XmlDocument.Load() 사용 (인코딩 안전)
        $node = $doc.DocumentElement.SelectSingleNode(&quot;*[local-name()='parent']&quot;)
        if (-not $node) { return $null }
        $g = $node.SelectSingleNode(&quot;*[local-name()='groupId']&quot;).InnerText.Trim()
        $a = $node.SelectSingleNode(&quot;*[local-name()='artifactId']&quot;).InnerText.Trim()
        $v = $node.SelectSingleNode(&quot;*[local-name()='version']&quot;).InnerText.Trim()
        if ($g -and $a -and $v) { return @($g, $a, $v) }
    } catch {}
    return $null
}

# BFS로 parent 체인 전체 수집
$seenParents = New-Object 'System.Collections.Generic.HashSet[string]'
$parentQueue = New-Object 'System.Collections.Generic.Queue[object]'

# 이미 수집된 POM에서 parent 목록 추출해서 큐에 넣기
$pomFiles = Get-ChildItem -Path $DEST_DIR -Recurse -Filter &quot;*.pom&quot;
foreach ($pf in $pomFiles) {
    $par = Get-PomParent $pf.FullName
    if ($par) {
        $key = &quot;$($par[0]):$($par[1]):$($par[2])&quot;
        if ($seenParents.Add($key)) {
            $parentQueue.Enqueue([PSCustomObject]@{ GroupId=$par[0]; ArtifactId=$par[1]; Version=$par[2] })
        }
    }
}

# BFS 실행
while ($parentQueue.Count -gt 0) {
    $p = $parentQueue.Dequeue()
    $srcPath = Find-M2Path -base &quot;$M2_DIR\$groupPath\$($p.ArtifactId)&quot; -version $p.Version
    if ($srcPath) {
        Copy-Item -Path &quot;$srcPath\*&quot; -Destination $dstDir -Recurse -Force
        Write-Host &quot;[PARENT] $($p.GroupId):$($p.ArtifactId):$($p.Version)&quot;
        # 복사한 POM의 parent도 큐에 추가
        $copiedPom = Get-ChildItem -Path $dstDir -Filter &quot;*.pom&quot; | Select-Object -First 1
        $par = Get-PomParent $copiedPom.FullName
        if ($par -and $seenParents.Add(&quot;$($par[0]):$($par[1]):$($par[2])&quot;)) {
            $parentQueue.Enqueue(...)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 삽질 포인트 &amp;mdash; [xml] 캐스트 vs XmlDocument.Load()&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 PowerShell의 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;[xml]$doc = Get-Content $pomPath&lt;/span&gt;로 파싱했는데 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Parent POMs copied: 0&lt;/span&gt;이라는 결과가 계속 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 두 가지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Get-Content&lt;/span&gt;는 파일 내용을 라인 단위 배열로 반환한다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;[xml]&lt;/span&gt; 캐스트에 넣으면 첫 번째 라인만 파싱되거나 인코딩 문제로 파싱 자체가 실패할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, POM 파일 인코딩이 UTF-8 BOM 없이 저장된 경우 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Get-Content&lt;/span&gt;의 기본 인코딩(UTF-16 LE)과 충돌한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;XmlDocument.Load()&lt;/span&gt;는 파일 경로를 직접 받아서 XML 선언의 인코딩(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;?xml ... encoding=&quot;UTF-8&quot;?&amp;gt;&lt;/span&gt;)을 자동으로 처리하므로 훨씬 안전하다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;powershell&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# 위험 (인코딩 문제 가능)
[xml]$doc = Get-Content $pomPath

# 안전
$doc = New-Object System.Xml.XmlDocument
$doc.Load($pomPath)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 결과&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상태&lt;/td&gt;
&lt;td&gt;수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JAR 의존성 (COPY)&lt;/td&gt;
&lt;td&gt;169&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;system scope (LOCAL)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parent POM 수집&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 55개의 parent POM이 누락되어 있었다.&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;만 믿으면 parent POM은 반드시 누락된다. JAR 수집 후 POM을 재귀적으로 파싱해서 parent 체인까지 챙겨야 한다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Java/Maven</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/295</guid>
      <comments>https://bumday.tistory.com/295#entry295comment</comments>
      <pubDate>Wed, 3 Jun 2026 10:22:18 +0900</pubDate>
    </item>
    <item>
      <title>[폐쇄망 CI/CD] Maven 의존성을 Nexus에 자동 업로드하기</title>
      <link>https://bumday.tistory.com/294</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;외부 인터넷이 차단된 폐쇄망 환경에서 Maven 프로젝트의 CI/CD를 구축하면서 겪은 의존성 관리 문제와 해결 과정을 정리한다.&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;&lt;b&gt;1. 배경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 보안 규정상 외부 인터넷이 막힌 폐쇄망 서버에 Spring Boot 기반 사내 시스템을 배포해야 했다. TeamCity로 CI/CD 파이프라인을 구성했는데, 빌드 서버가 Maven Central에 접근할 수 없으니 의존성을 어디선가 가져와야 했다.&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;b&gt;Nexus Repository Manager&lt;/b&gt; 구축&lt;/li&gt;
&lt;li&gt;개발자 PC의 로컬 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt; 캐시에서 필요한 의존성만 추려서 Nexus에 업로드&lt;/li&gt;
&lt;li&gt;CI/CD 빌드 서버는 Nexus만 바라보도록 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;settings.xml&lt;/span&gt; 설정&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 환경 정보&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&lt;/td&gt;
&lt;td&gt;값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nexus URL&lt;/td&gt;
&lt;td&gt;내부망 Nexus 서버 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;업로드 레포&lt;/td&gt;
&lt;td&gt;hosted 타입 레포 (프로젝트 전용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;그룹 레포&lt;/td&gt;
&lt;td&gt;maven-public (업로드 레포 포함)&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;172개 JAR + 55개 parent POM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD 서버의 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;settings.xml&lt;/span&gt;은 그룹 레포만 바라보면 된다. 개별 레포를 일일이 추가할 필요 없이 그룹 하나로 통합 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 스크립트 구성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 두 단계 PowerShell 스크립트로 분리했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;cicd/nexus/
├── 01_collect-mes-deps.ps1   # 의존성 수집 (인터넷 연결된 PC에서 실행)
├── 02_upload-to-nexus.ps1    # Nexus 업로드
└── README.md&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. Step 1 &amp;mdash; 의존성 수집&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;로 프로젝트 의존성 목록을 추출한 뒤, 각 항목을 로컬 .m2에서 찾아 임시 수집 경로로 복사하는 방식이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;# mvn으로 의존성 목록 추출
mvn dependency:list &quot;-DoutputFile=C:\temp\my-project-deps.txt&quot; -DincludeScope=test -q

# .m2에서 수집 경로로 복사
foreach ($line in $depLines) {
    $parts      = $line.Trim() -split &quot;:&quot;
    $groupId    = $parts[0]
    $artifactId = $parts[1]
    $version    = $parts[-2]
    $srcPath    = Find-M2Path -base &quot;$M2_DIR\$groupPath\$artifactId&quot; -version $version

    if ($srcPath) {
        Copy-Item -Path &quot;$srcPath\*&quot; -Destination $dstPath -Recurse -Force
        Write-Host &quot;COPY ${groupId}:${artifactId}:${version}&quot;
    } else {
        $missed += ... # 나중에 처리
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수집 후에는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.lastUpdated&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;_remote.repositories&lt;/span&gt; 같은 불필요한 메타파일을 제거했다. 이런 파일들이 Nexus에 올라가면 이후 빌드에서 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;는 JAR 아티팩트만 출력하고 &lt;b&gt;parent POM은 누락&lt;/b&gt;한다. 이 때문에 빌드가 실패했고 별도 로직으로 해결했다. (&lt;a href=&quot;https://bumday.tistory.com/294&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;다음 포스팅&lt;/a&gt;에서 자세히 다룬다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4-1. system scope 의존성 처리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;pom.xml&lt;/span&gt;에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;lt;scope&amp;gt;system&amp;lt;/scope&amp;gt;&lt;/span&gt;으로 선언된 로컬 JAR는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.m2&lt;/span&gt;에 없으므로 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;systemPath&lt;/span&gt;에서 직접 복사했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;powershell&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# pom.xml의 system scope 의존성을 systemPath에서 복사
foreach ($dep in $pom.project.dependencies.dependency) {
    if ($dep.scope -eq &quot;system&quot;) {
        $path = $dep.systemPath -replace '\$\{project\.basedir\}', $PROJECT_ROOT
        Copy-Item -Path $path -Destination $jarDst -Force
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. Step 2 &amp;mdash; Nexus 업로드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수집 경로의 파일들을 Nexus REST API(PUT)로 업로드했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;powershell&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;$url    = &quot;$NEXUS_URL/repository/maven-my-project-deps/$relativePath&quot;
$result = Invoke-WebRequest -Uri $url -Method PUT -InFile $file.FullName `
    -Headers @{ Authorization = &quot;Basic $cred&quot; } -UseBasicParsing

if ($result.StatusCode -eq 201) {
    Add-Content $DONE_LOG $relativePath  # 완료 기록
    Write-Host &quot;OK  $relativePath&quot; -ForegroundColor Green
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 재시작 내성 기능이다. 완료된 파일 경로를 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;upload-done.log&lt;/span&gt;에 기록해두고, 재실행 시 이미 올라간 파일은 SKIP한다. 네트워크가 불안정해 중간에 끊겨도 이어서 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. CI/CD 빌드 서버 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TeamCity 빌드 에이전트의 Maven settings.xml에 Nexus 미러 설정만 추가하면 된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;xml&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;&amp;lt;settings&amp;gt;
  &amp;lt;mirrors&amp;gt;
    &amp;lt;mirror&amp;gt;
      &amp;lt;id&amp;gt;nexus&amp;lt;/id&amp;gt;
      &amp;lt;mirrorOf&amp;gt;*&amp;lt;/mirrorOf&amp;gt;
      &amp;lt;url&amp;gt;http://nexus.internal:8081/repository/maven-public/&amp;lt;/url&amp;gt;
    &amp;lt;/mirror&amp;gt;
  &amp;lt;/mirrors&amp;gt;
  &amp;lt;servers&amp;gt;
    &amp;lt;server&amp;gt;
      &amp;lt;id&amp;gt;nexus&amp;lt;/id&amp;gt;
      &amp;lt;username&amp;gt;배포용_계정&amp;lt;/username&amp;gt;
      &amp;lt;password&amp;gt;패스워드&amp;lt;/password&amp;gt;
    &amp;lt;/server&amp;gt;
  &amp;lt;/servers&amp;gt;
&amp;lt;/settings&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;172개 JAR 의존성과 55개 parent POM 수집을 완료했고, 전체 Nexus 업로드에 성공했다. 최종적으로 폐쇄망 TeamCity 빌드까지 통과했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 parent POM 누락 문제와 로그 충돌 문제 두 가지 추가 트러블슈팅이 있었다. 각각 별도 포스팅으로 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;(&lt;span style=&quot;color: #666666;&quot;&gt;parent POM 누락문제 포스팅:&lt;/span&gt; &lt;a href=&quot;https://bumday.tistory.com/294&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bumday.tistory.com/294&lt;/a&gt;)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;(&lt;span style=&quot;color: #666666;&quot;&gt;로그 충돌 문제 포스팅:&lt;/span&gt; &lt;a href=&quot;https://bumday.tistory.com/295&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bumday.tistory.com/295&lt;/a&gt;)&lt;/i&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;p data-ke-size=&quot;size16&quot;&gt;폐쇄망 Maven 빌드에서 의존성 문제의 핵심은 JAR 외에 parent POM까지 챙기는 것이다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;mvn dependency:list&lt;/span&gt;만 믿으면 반드시 빌드가 깨진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/Maven</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/294</guid>
      <comments>https://bumday.tistory.com/294#entry294comment</comments>
      <pubDate>Wed, 3 Jun 2026 10:18:10 +0900</pubDate>
    </item>
    <item>
      <title>[Troubleshooting] Spring Boot 2.3 + JEUS 로그 라이브러리 충돌 해결</title>
      <link>https://bumday.tistory.com/293</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 2.3 기반 프로젝트를 JEUS에 배포하는 과정에서 로그 라이브러리 충돌 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특이한 점은 로컬에서 src 폴더를 직접 구동할 때는 아무 문제가 없었다는 것이다. 빌드된 target 폴더를 실행하거나 JEUS에 배포할 때만 문제가 생겼다.&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;&lt;b&gt;1. 문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 Smart Tomcat에서 소스(src) 폴더를 직접 지정해 실행하면 정상 동작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 빌드 결과물인 target 폴더를 실행 대상으로 잡거나 운영 서버(JEUS)에 배포하면 Multiple SLF4J bindings 에러와 함께 기동이 실패했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777947480722&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/.../logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/.../log4j-slf4j-impl-2.17.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 원인 분석&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드 실행과 빌드 결과물 실행의 차이에서 오는 의존성 충돌 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;src&lt;/span&gt; 모드에서는 IntelliJ가 클래스패스를 관리하며 중복된 라이브러리 중 하나에 임의로 우선순위를 두어 실행한다. 덕분에 충돌이 잠복해 있어도 표면에 드러나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Maven이 빌드를 수행하면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;pom.xml&lt;/span&gt;에 정의된 모든 의존성을 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;WEB-INF/lib&lt;/span&gt;으로 물리적으로 복사한다. 이때 의존성 전이(Transitive Dependency)로 끌려온 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;log4j-slf4j-impl&lt;/span&gt;이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;logback&lt;/span&gt;과 같은 폴더에 놓이면서 물리적 충돌이 비로소 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JEUS는 클래스패스 내 모든 JAR를 엄격하게 검사하기 때문에 중복 바인딩이 발견되면 기동 자체를 거부한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 결과물에 불필요한 라이브러리가 포함되지 않도록 강제 조치해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방법 1: Provided Scope 활용 (추천)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log4j-slf4j-impl이 어떤 경로로 유입되는지 특정하기 어려울 때 가장 확실한 방법이다. pom.xml에 해당 의존성을 명시하고 배포 시 제외되도록 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777947550126&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.logging.log4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;log4j-slf4j-impl&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.17.1&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방법 2: Exclusion 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유입 경로를 확인했다면 상위 라이브러리 설정에서 직접 해당 모듈을 제외한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777947564536&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
    &amp;lt;exclusions&amp;gt;
        &amp;lt;exclusion&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.logging.log4j&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;log4j-slf4j-impl&amp;lt;/artifactId&amp;gt;
        &amp;lt;/exclusion&amp;gt;
    &amp;lt;/exclusions&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-test-render-count=&quot;1&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;src&lt;/span&gt; 폴더 실행 시 문제가 없다고 해서 의존성이 깨끗한 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;provided&lt;/span&gt; 스코프는 &quot;서버가 제공할 것이니 빌드 결과물에는 넣지 마라&quot;는 의미다. 원치 않는 중복 라이브러리가 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;target&lt;/span&gt;에 유입되는 것을 막는 가장 확실한 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 후 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;WEB-INF/lib&lt;/span&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&gt;
&lt;div data-state=&quot;closed&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div data-state=&quot;closed&quot;&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;</description>
      <category>Java/Maven</category>
      <author>범데이</author>
      <guid isPermaLink="true">https://bumday.tistory.com/293</guid>
      <comments>https://bumday.tistory.com/293#entry293comment</comments>
      <pubDate>Tue, 5 May 2026 11:20:45 +0900</pubDate>
    </item>
  </channel>
</rss>